columns = new ArrayList<>(List.of(FieldKey.fromParts("Version"), FieldKey.fromParts("Created"), FieldKey.fromParts("Link"), FieldKey.fromParts("Share")));
@@ -9741,7 +9759,8 @@ public static class CreatePanoramaPublicMessageAction extends SimpleViewAction
+ {
+ @Override
+ public void validateCommand(PrivateDataReminderSettingsForm form, Errors errors)
+ {
+ if (form.getDelayUntilFirstReminder() == null)
+ {
+ errors.reject(ERROR_MSG, "Please enter a value for 'Delay until first reminder'.");
+ }
+ else if (form.getDelayUntilFirstReminder() < 0)
+ {
+ errors.reject(ERROR_MSG, "Value for 'Delay until first reminder' cannot be less than 0.");
+ }
+ if (form.getReminderFrequency() == null)
+ {
+ errors.reject(ERROR_MSG, "Please enter a value for 'Reminder frequency'.");
+ }
+ else if (form.getReminderFrequency() < 0)
+ {
+ errors.reject(ERROR_MSG, "Value for 'Reminder frequency' cannot be less than 0.");
+ }
+ if (form.getExtensionLength() == null)
+ {
+ errors.reject(ERROR_MSG, "Please enter a value for 'Extension duration'.");
+ }
+ else if (form.getExtensionLength() < 0)
+ {
+ errors.reject(ERROR_MSG, "Value for 'Extension duration' cannot be less than 0.");
+ }
+ if (form.getReminderTime() == null)
+ {
+ errors.reject(ERROR_MSG, "Please enter a value for 'Reminder time'.");
+ }
+ else if (PrivateDataReminderSettings.parseReminderTime(form.getReminderTime()) == null)
+ {
+ errors.reject(ERROR_MSG, String.format("'Reminder time' could not be parsed. It must be in the format - %s, e.g. %s.",
+ PrivateDataReminderSettings.REMINDER_TIME_FORMAT, PrivateDataReminderSettings.DEFAULT_REMINDER_TIME));
+ }
+ }
+
+ @Override
+ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow, BindException errors) throws Exception
+ {
+ if (!reshow)
+ {
+ PrivateDataReminderSettings settings = PrivateDataReminderSettings.get();
+ form.setEnabled(settings.isEnableReminders());
+ form.setReminderTime(settings.getReminderTimeFormatted());
+ form.setDelayUntilFirstReminder(settings.getDelayUntilFirstReminder());
+ form.setReminderFrequency(settings.getReminderFrequency());
+ form.setExtensionLength(settings.getExtensionLength());
+ }
+
+ VBox view = new VBox();
+ view.addView(new JspView<>("/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp", form, errors));
+ view.setTitle("Private Data Reminder Settings");
+ view.setFrame(WebPartView.FrameType.PORTAL);
+ return view;
+ }
+
+ @Override
+ public boolean handlePost(PrivateDataReminderSettingsForm form, BindException errors) throws Exception
+ {
+ PrivateDataReminderSettings settings = new PrivateDataReminderSettings();
+ settings.setEnableReminders(form.isEnabled());
+ settings.setReminderTime(PrivateDataReminderSettings.parseReminderTime(form.getReminderTime()));
+ settings.setDelayUntilFirstReminder(form.getDelayUntilFirstReminder());
+ settings.setReminderFrequency(form.getReminderFrequency());
+ settings.setExtensionLength(form.getExtensionLength());
+ PrivateDataReminderSettings.save(settings);
+
+ PrivateDataMessageScheduler.getInstance().initialize(settings.isEnableReminders());
+ return true;
+ }
+
+ @Override
+ public URLHelper getSuccessURL(PrivateDataReminderSettingsForm privateDataReminderSettingsForm)
+ {
+ return null;
+ }
+
+ @Override
+ public ModelAndView getSuccessView(PrivateDataReminderSettingsForm form)
+ {
+ ActionURL url = new ActionURL(PrivateDataReminderSettingsAction.class, getContainer());
+ return new HtmlView(
+ DIV("Private data reminder settings saved!",
+ BR(),
+ new LinkBuilder("Back to Private Data Reminder Settings").href(url).build()));
+ }
+
+ @Override
+ public void addNavTrail(NavTree root)
+ {
+ addPanoramaPublicAdminConsoleNav(root, getContainer());
+ root.addChild("Private Data Reminder Settings");
+ }
+ }
+
+ public static class PrivateDataReminderSettingsForm
+ {
+ private boolean _enabled;
+ private String _reminderTime;
+ private Integer _extensionLength;
+ private Integer _reminderFrequency;
+ private Integer _delayUntilFirstReminder;
+
+ public boolean isEnabled()
+ {
+ return _enabled;
+ }
+
+ public void setEnabled(boolean enabled)
+ {
+ _enabled = enabled;
+ }
+
+ public String getReminderTime()
+ {
+ return _reminderTime;
+ }
+
+ public void setReminderTime(String reminderTime)
+ {
+ _reminderTime = reminderTime;
+ }
+
+ public Integer getExtensionLength()
+ {
+ return _extensionLength;
+ }
+
+ public void setExtensionLength(Integer extensionLength)
+ {
+ _extensionLength = extensionLength;
+ }
+
+ public Integer getReminderFrequency()
+ {
+ return _reminderFrequency;
+ }
+
+ public void setReminderFrequency(Integer reminderFrequency)
+ {
+ _reminderFrequency = reminderFrequency;
+ }
+
+ public Integer getDelayUntilFirstReminder()
+ {
+ return _delayUntilFirstReminder;
+ }
+
+ public void setDelayUntilFirstReminder(Integer delayUntilFirstReminder)
+ {
+ _delayUntilFirstReminder = delayUntilFirstReminder;
+ }
+ }
+
+ @RequiresPermission(AdminOperationsPermission.class)
+ public class SendPrivateDataRemindersAction extends FormViewAction
+ {
+ @Override
+ public void validateCommand(PrivateDataSendReminderForm form, Errors errors){}
+
+ @Override
+ public ModelAndView getView(PrivateDataSendReminderForm form, boolean reshow, BindException errors) throws Exception
+ {
+ Journal panoramaPublic = JournalManager.getJournal(getContainer());
+ if (panoramaPublic == null)
+ {
+ errors.reject(ERROR_MSG, "Not a Panorama Public folder: " + getContainer().getName());
+ return new SimpleErrorView(errors, true);
+ }
+
+ QuerySettings qSettings = new QuerySettings(getViewContext(), PanoramaPublicSchema.TABLE_EXPERIMENT_ANNOTATIONS,
+ PanoramaPublicSchema.TABLE_EXPERIMENT_ANNOTATIONS);
+ qSettings.setContainerFilterName(ContainerFilter.Type.CurrentAndSubfolders.name());
+ qSettings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("Public"), "No"));
+
+ QueryView tableView = new QueryView(new PanoramaPublicSchema(getUser(), getContainer()), qSettings, null);
+ tableView.setTitle("Private Panorama Private Datasets");
+ tableView.setFrame(WebPartView.FrameType.NONE);
+ tableView.disableContainerFilterSelection();
+
+ form.setDataRegionName(tableView.getDataRegionName());
+
+ JspView jspView = new JspView<>("/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp", form, errors);
+ VBox view = new VBox(jspView, tableView);
+ view.setTitle("Send Reminders");
+ view.setFrame(WebPartView.FrameType.PORTAL);
+ return view;
+ }
+
+ @Override
+ public boolean handlePost(PrivateDataSendReminderForm form, BindException errors) throws Exception
+ {
+ List selectedExperimentIds = form.getSelectedExperimentIds();
+ if (selectedExperimentIds.isEmpty())
+ {
+ errors.reject(ERROR_MSG, "Please select at least one experiment");
+ return false;
+ }
+ PipelineJob job = new PrivateDataReminderJob(getViewBackgroundInfo(),
+ PipelineService.get().getPipelineRootSetting(getContainer()),
+ JournalManager.getJournal(getContainer()),
+ form.getSelectedExperimentIds(),
+ form.getTestMode());
+ PipelineService.get().queueJob(job);
+ return true;
+ }
+
+
+ @Override
+ public URLHelper getSuccessURL(PrivateDataSendReminderForm form)
+ {
+ return PageFlowUtil.urlProvider(PipelineUrls.class).urlBegin(getContainer());
+ }
+
+ @Override
+ public void addNavTrail(NavTree root)
+ {
+ root.addChild("Private Data Reminder Settings", new ActionURL(PrivateDataReminderSettingsAction.class, ContainerManager.getRoot()));
+ root.addChild("Send Reminders");
+ }
+ }
+
+ public static class PrivateDataSendReminderForm
+ {
+ private boolean _testMode;
+ private String _selectedIds;
+ private String _dataRegionName = null;
+
+ public boolean getTestMode()
+ {
+ return _testMode;
+ }
+
+ public void setTestMode(boolean testMode)
+ {
+ _testMode = testMode;
+ }
+
+ public String getSelectedIds()
+ {
+ return _selectedIds;
+ }
+
+ public List getSelectedExperimentIds()
+ {
+ if (_selectedIds == null)
+ {
+ return Collections.emptyList();
+ }
+ return Arrays.stream(StringUtils.split(_selectedIds, ",")).map(Integer::parseInt).collect(Collectors.toList());
+ }
+
+ public void setSelectedIds(String selectedIds)
+ {
+ _selectedIds = selectedIds;
+ }
+
+ public String getDataRegionName()
+ {
+ return _dataRegionName;
+ }
+
+ public void setDataRegionName(String dataRegionName)
+ {
+ _dataRegionName = dataRegionName;
+ }
+ }
+
+ @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class})
+ public abstract class UpdateDatasetStatusAction extends ConfirmAction
+ {
+ protected ExperimentAnnotations _exptAnnotations;
+ protected DatasetStatus _datasetStatus;
+
+ protected abstract void doValidationForAction(Errors errors);
+ protected abstract void updateDatasetStatus(DatasetStatus datasetStatus);
+ protected abstract void postNotification();
+ protected abstract String getConfirmViewTitle();
+ protected abstract String getConfirmViewMessage();
+
+ public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception
+ {
+ setTitle(getConfirmViewTitle());
+ HtmlView view = new HtmlView(DIV(
+ DIV(getConfirmViewMessage()),
+ DIV("Title: " + _exptAnnotations.getTitle()),
+ DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), PrivateDataReminderSettings.DATE_FORMAT_PATTERN)),
+ DIV("Submitter: " + _exptAnnotations.getSubmitterName())
+ ));
+ view.setTitle(getConfirmViewTitle());
+ return view;
+ }
+
+ @Override
+ public void validateCommand(ShortUrlForm shortUrlForm, Errors errors)
+ {
+ _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, errors);
+ if (_exptAnnotations == null)
+ {
+ return;
+ }
+
+ ensureCorrectContainer(getContainer(), _exptAnnotations.getContainer(), getViewContext());
+
+ _datasetStatus = DatasetStatusManager.getForExperiment(_exptAnnotations);
+
+ // Action-specific validation
+ doValidationForAction(errors);
+ }
+
+ @Override
+ public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throws Exception
+ {
+ try(DbScope.Transaction transaction = CoreSchema.getInstance().getSchema().getScope().ensureTransaction())
+ {
+ if (_datasetStatus == null)
+ {
+ _datasetStatus = new DatasetStatus();
+ _datasetStatus.setExperimentAnnotationsId(_exptAnnotations.getId());
+ updateDatasetStatus(_datasetStatus);
+ DatasetStatusManager.save(_datasetStatus, getUser());
+ }
+ else
+ {
+ updateDatasetStatus(_datasetStatus);
+ DatasetStatusManager.update(_datasetStatus, getUser());
+ }
+
+ postNotification();
+
+ transaction.commit();
+ }
+
+ return true;
+ }
+
+ @Override
+ public @NotNull URLHelper getSuccessURL(ShortUrlForm shortUrlForm)
+ {
+ return PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer());
+ }
+ }
+
+ @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class})
+ public class RequestExtensionAction extends UpdateDatasetStatusAction
+ {
+ @Override
+ protected String getConfirmViewTitle()
+ {
+ return "Request Extension For Panorama Public Data";
+ }
+
+ @Override
+ protected String getConfirmViewMessage()
+ {
+ return "You are requesting an extension for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL();
+ }
+
+ @Override
+ protected void doValidationForAction(Errors errors)
+ {
+ if (_datasetStatus != null)
+ {
+ PrivateDataReminderSettings settings = PrivateDataReminderSettings.get();
+ if (settings.isExtensionValid(_datasetStatus))
+ {
+ errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + _exptAnnotations.getShortUrl().renderShortURL()
+ + ". The extension is valid until " + settings.extensionValidUntilFormatted(_datasetStatus));
+ }
+ else if (_datasetStatus.deletionRequested())
+ {
+ errors.reject(ERROR_MSG, "A deletion request was submitted on " + _datasetStatus.getDeletionRequestedDate()
+ + " for the data with short URL " + _exptAnnotations.getShortUrl().renderShortURL());
+ }
+ }
+ }
+
+ @Override
+ protected void updateDatasetStatus(DatasetStatus datasetStatus)
+ {
+ datasetStatus.setExtensionRequestedDate(new Date());
+ }
+
+ @Override
+ protected void postNotification()
+ {
+ // Post a message to the support thread.
+ JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(_exptAnnotations);
+ Journal journal = JournalManager.getJournal(submission.getJournalId());
+ PanoramaPublicNotification.postPrivateStatusExtensionMessage(journal, submission.getJournalExperiment(), _exptAnnotations, getUser());
+ }
+
+ @Override
+ public ModelAndView getSuccessView(ShortUrlForm shortUrlForm)
+ {
+ setTitle("Extension Request Success");
+ PrivateDataReminderSettings settings = PrivateDataReminderSettings.get();
+ return new HtmlView(DIV("An extension request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(),
+ DIV("The extension is valid until " + settings.extensionValidUntilFormatted(_datasetStatus)),
+ BR(),
+ DIV(
+ LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer()))
+ )
+ ));
+ }
+ }
+
+ @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class})
+ public class RequestDeletionAction extends UpdateDatasetStatusAction
+ {
+ @Override
+ protected String getConfirmViewTitle()
+ {
+ return "Request Deletion For Panorama Public Data";
+ }
+
+ @Override
+ protected String getConfirmViewMessage()
+ {
+ return "You are requesting deletion of the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL();
+ }
+
+ @Override
+ protected void doValidationForAction(Errors errors)
+ {
+ if (_datasetStatus != null)
+ {
+ if (_datasetStatus.deletionRequested())
+ {
+ errors.reject(ERROR_MSG, "A deletion request was already submitted on " + _datasetStatus.getDeletionRequestedDateFormatted()
+ + " for the data with short URL " + _exptAnnotations.getShortUrl().renderShortURL());
+ }
+ }
+ }
+
+ @Override
+ protected void updateDatasetStatus(DatasetStatus datasetStatus)
+ {
+ datasetStatus.setDeletionRequestedDate(new Date());
+ }
+
+ @Override
+ protected void postNotification()
+ {
+ // Post a message to the support thread.
+ JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(_exptAnnotations);
+ Journal journal = JournalManager.getJournal(submission.getJournalId());
+ PanoramaPublicNotification.postDataDeletionRequestMessage(journal, submission.getJournalExperiment(), _exptAnnotations, getUser());
+ }
+
+ @Override
+ public ModelAndView getSuccessView(ShortUrlForm shortUrlForm)
+ {
+ setTitle("Deletion Request Success");
+ return new HtmlView(DIV("A deletion request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(),
+ BR(),
+ DIV(
+ LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer()))
+ )
+ ));
+ }
+ }
+
+ public static class ShortUrlForm
+ {
+ private String _shortUrlEntityId;
+
+ public String getShortUrlEntityId()
+ {
+ return _shortUrlEntityId;
+ }
+
+ public void setShortUrlEntityId(String shortUrlEntityId)
+ {
+ _shortUrlEntityId = shortUrlEntityId;
+ }
+ }
+
+ private static ExperimentAnnotations getValidExperimentAnnotations(ShortUrlForm shortUrlForm, Errors errors)
+ {
+ String shortUrlEntityId = shortUrlForm.getShortUrlEntityId();
+ if (StringUtils.isBlank(shortUrlEntityId))
+ {
+ errors.reject(ERROR_MSG, "ShortUrl is missing");
+ return null;
+ }
+
+ ShortURLRecord shortUrl = ShortURLService.get().getForEntityId(shortUrlEntityId);
+ if (shortUrl == null)
+ {
+ errors.reject(ERROR_MSG, "Cannot find a shortUrl for entityId " + shortUrlEntityId);
+ return null;
+ }
+
+ ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentForShortUrl(shortUrl);
+ if (exptAnnotations == null)
+ {
+ errors.reject(ERROR_MSG, "Unable to find an experiment for short URL: " + shortUrl.renderShortURL());
+ return null;
+ }
+
+ if (exptAnnotations.isPublic())
+ {
+ errors.reject(ERROR_MSG, "Data for short URL " + shortUrl.renderShortURL() + " is public. Status cannot be changed.");
+ return null;
+ }
+
+ return exptAnnotations;
+ }
+
public static ActionURL getCopyExperimentURL(int experimentAnnotationsId, int journalId, Container container)
{
ActionURL result = new ActionURL(PanoramaPublicController.CopyExperimentAction.class, container);
@@ -10216,7 +10748,8 @@ public void testActionPermissions()
new DeleteJournalGroupAction(),
new GetPxActionsAction(),
new ExportPxXmlAction(),
- new UpdatePxDetailsAction()
+ new UpdatePxDetailsAction(),
+ new PrivateDataReminderSettingsAction()
);
// @AdminConsoleAction
diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicManager.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicManager.java
index e5338cd6..f959904d 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicManager.java
+++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicManager.java
@@ -125,6 +125,11 @@ public static TableInfo getTableInfoCatalogEntry()
return getSchema().getTable(PanoramaPublicSchema.TABLE_CATALOG_ENTRY);
}
+ public static TableInfo getTableInfoDatasetStatus()
+ {
+ return getSchema().getTable(PanoramaPublicSchema.TABLE_DATASET_STATUS);
+ }
+
public static ITargetedMSRun getRunByLsid(String lsid, Container container)
{
return TargetedMSService.get().getRunByLsid(lsid, container);
diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java
index 31cbe681..6e4d2c6c 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java
+++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java
@@ -46,6 +46,7 @@
import org.labkey.panoramapublic.bluesky.BlueskyApiClient;
import org.labkey.panoramapublic.bluesky.PanoramaPublicLogoResourceType;
import org.labkey.panoramapublic.catalog.CatalogImageAttachmentType;
+import org.labkey.panoramapublic.message.PrivateDataReminderSettings;
import org.labkey.panoramapublic.model.Journal;
import org.labkey.panoramapublic.model.speclib.SpecLibKey;
import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineProvider;
@@ -91,7 +92,7 @@ public String getName()
@Override
public @Nullable Double getSchemaVersion()
{
- return 25.001;
+ return 25.003;
}
@Override
@@ -382,6 +383,7 @@ public Set getUnitTests()
set.add(Formula.TestCase.class);
set.add(CatalogEntryManager.TestCase.class);
set.add(BlueskyApiClient.TestCase.class);
+ set.add(PrivateDataReminderSettings.TestCase.class);
return set;
}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java
index a6660883..2bb66369 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java
+++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java
@@ -10,21 +10,26 @@
import org.labkey.api.announcements.api.AnnouncementService;
import org.labkey.api.data.Container;
import org.labkey.api.data.ContainerManager;
+import org.labkey.api.markdown.MarkdownService;
import org.labkey.api.portal.ProjectUrls;
import org.labkey.api.security.User;
import org.labkey.api.security.UserManager;
import org.labkey.api.settings.AppProps;
import org.labkey.api.settings.LookAndFeelProperties;
+import org.labkey.api.util.DateUtil;
import org.labkey.api.util.PageFlowUtil;
import org.labkey.api.view.ActionURL;
import org.labkey.api.view.NotFoundException;
import org.labkey.panoramapublic.datacite.DataCiteException;
import org.labkey.panoramapublic.datacite.DataCiteService;
+import org.labkey.panoramapublic.message.PrivateDataReminderSettings;
import org.labkey.panoramapublic.model.ExperimentAnnotations;
import org.labkey.panoramapublic.model.Journal;
import org.labkey.panoramapublic.model.JournalExperiment;
+import org.labkey.panoramapublic.model.JournalSubmission;
import org.labkey.panoramapublic.model.Submission;
import org.labkey.panoramapublic.proteomexchange.ProteomeXchangeService;
+import org.labkey.panoramapublic.query.ExperimentAnnotationsManager;
import org.labkey.panoramapublic.query.JournalManager;
import org.labkey.panoramapublic.query.SubmissionManager;
@@ -292,6 +297,130 @@ public static StringBuilder getFullMessageBody(String text, User messageTo, User
return messageBody;
}
+ public static void postPrivateStatusExtensionMessage(@NotNull Journal journal, @NotNull JournalExperiment je, @NotNull ExperimentAnnotations expAnnotations, User submitter)
+ {
+ User journalAdmin = JournalManager.getJournalAdminUser(journal);
+ if (journalAdmin == null)
+ {
+ throw new NotFoundException(String.format("Could not find an admin user for %s.", journal.getName()));
+ }
+ PrivateDataReminderSettings reminderSettings = PrivateDataReminderSettings.get();
+
+ String messageTitle = "Private Status Extended" +" - " + je.getShortAccessUrl().renderShortURL();
+ StringBuilder messageBody = new StringBuilder();
+ messageBody.append("Dear ").append(getUserName(submitter)).append(",").append(NL2);
+ messageBody.append("Thank you for your request to extend the private status of your data on Panorama Public. ")
+ .append("Your data has been granted a " + reminderSettings.getExtensionLength() + " month extension. ")
+ .append("You will receive another reminder when this period ends. ")
+ .append("If you'd like to make your data public sooner, you can do so at any time ")
+ .append("by clicking the \"Make Public\" button in your data folder, or by clicking this link: ")
+ .append(bold(link("Make Data Public", PanoramaPublicController.getMakePublicUrl(expAnnotations.getId(), expAnnotations.getContainer()).getURIString())))
+ .append(".");
+ messageBody.append(NL2).append("Best regards,");
+ messageBody.append(NL).append(getUserName(journalAdmin));
+
+ postNotificationFullTitle(journal, je, messageBody.toString(), journalAdmin, messageTitle, StatusOption.Closed, null);
+ }
+
+ public static void postDataDeletionRequestMessage(@NotNull Journal journal, @NotNull JournalExperiment je, @NotNull ExperimentAnnotations expAnnotations, User submitter)
+ {
+ User journalAdmin = JournalManager.getJournalAdminUser(journal);
+ if (journalAdmin == null)
+ {
+ throw new NotFoundException(String.format("Could not find an admin user for %s.", journal.getName()));
+ }
+
+ ExperimentAnnotations sourceExperiment = ExperimentAnnotationsManager.get(expAnnotations.getSourceExperimentId());
+
+ String messageTitle = "Data Deletion Requested" +" - " + expAnnotations.getShortUrl().renderShortURL();
+
+ StringBuilder messageBody = new StringBuilder();
+ messageBody.append("Dear ").append(getUserName(submitter)).append(",").append(NL2);
+ messageBody.append("Thank you for your request to delete your data on Panorama Public. ")
+ .append("We will remove your data from Panorama Public. ");
+ if (sourceExperiment != null)
+ {
+ messageBody.append("Your source folder ")
+ .append(getContainerLink(sourceExperiment.getContainer()))
+ .append(" will remain intact, allowing you to resubmit the data in the future if you wish. ");
+ }
+ else
+ {
+ messageBody.append("We were unable to locate the source folder for this data in your project. ")
+ .append("The folder at the path ")
+ .append(expAnnotations.getSourceExperimentPath())
+ .append(" may have been deleted.");
+ }
+
+ messageBody.append(NL2).append("Best regards,");
+ messageBody.append(NL).append(getUserName(journalAdmin));
+
+ postNotificationFullTitle(journal, je, messageBody.toString(), journalAdmin, messageTitle, StatusOption.Active, null);
+ }
+
+
+ public static void postPrivateDataReminderMessage(@NotNull Journal journal, @NotNull JournalSubmission js, @NotNull ExperimentAnnotations expAnnotations,
+ @NotNull User submitter, @NotNull User messagePoster, List notifyUsers,
+ @NotNull Announcement announcement, @NotNull Container announcementsContainer, @NotNull User journalAdmin)
+ {
+ String message = getDataStatusReminderMessage(expAnnotations, submitter, js, announcement, announcementsContainer, journalAdmin);
+ String title = "Action Required: Status Update for Your Private Data on Panorama Public";
+ postNotificationFullTitle(journal, js.getJournalExperiment(), message, messagePoster, title, StatusOption.Closed, notifyUsers);
+ }
+
+ public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations exptAnnotations, @NotNull User submitter,
+ @NotNull JournalSubmission js,@NotNull Announcement announcement,
+ @NotNull Container announcementContainer, @NotNull User journalAdmin)
+ {
+ String shortUrl = exptAnnotations.getShortUrl().renderShortURL();
+ String makePublicLink = PanoramaPublicController.getMakePublicUrl(exptAnnotations.getId(), exptAnnotations.getContainer()).getURIString();
+ String dateString = DateUtil.formatDateTime(js.getLatestSubmission().getCreated(), PrivateDataReminderSettings.DATE_FORMAT_PATTERN);
+
+ ActionURL viewMessageUrl = new ActionURL("announcements", "thread", announcementContainer)
+ .addParameter("rowId", announcement.getRowId());
+ ActionURL respondToMessageUrl = new ActionURL("announcements", "respond", announcementContainer)
+ .addParameter("parentId", announcement.getEntityId())
+ .addReturnUrl(viewMessageUrl);
+
+ String shortUrlEntityId = exptAnnotations.getShortUrl().getEntityId().toString();
+ ActionURL requestExtensionUrl = new ActionURL(PanoramaPublicController.RequestExtensionAction.class, exptAnnotations.getContainer())
+ .addParameter("shortUrlEntityId", shortUrlEntityId);
+
+ ActionURL requesDeletionUrl = new ActionURL(PanoramaPublicController.RequestDeletionAction.class, exptAnnotations.getContainer())
+ .addParameter("shortUrlEntityId",shortUrlEntityId);
+
+
+ ExperimentAnnotations sourceExperiment = ExperimentAnnotationsManager.get(exptAnnotations.getSourceExperimentId());
+
+ StringBuilder message = new StringBuilder();
+ message.append("Dear ").append(getUserName(submitter)).append(",").append(NL2)
+ .append("We are reaching out regarding your data on Panorama Public (").append(shortUrl).append("), which has been private since ")
+ .append(dateString).append(".")
+ .append("\n\n**Is the paper associated with this work already published?**")
+ .append("\n- If yes: Please make your data public by clicking the \"Make Public\" button in your folder or by clicking this link: ")
+ .append(bold(link("Make Data Public", makePublicLink)))
+ .append(". This helps ensure that your valuable research is easily accessible to the community.")
+ .append("\n- If not: You have a couple of options:")
+ .append("\n - **Request an Extension** - If your paper is still under review, or you need additional time, please let us know by clicking ")
+ .append(bold(link("Request Extension", requestExtensionUrl.getURIString()))).append(".")
+ .append("\n - **Delete from Panorama Public** - If you no longer wish to host your data on Panorama Public, please click ")
+ .append(bold(link("Request Deletion", requesDeletionUrl.getURIString()))).append(". ")
+ .append("We will remove your data from Panorama Public.");
+ if (sourceExperiment != null)
+ {
+ message.append(" However, your source folder (")
+ .append(getContainerLink(sourceExperiment.getContainer()))
+ .append(") will remain intact, allowing you to resubmit your data in the future if you wish.");
+ }
+
+ message.append("\n\nIf you have any questions or need further assistance, please do not hesitate to respond to this message by ")
+ .append(bold(link("clicking here", respondToMessageUrl.getURIString()))).append(".")
+ .append("\n\nThank you for sharing your research on Panorama Public. We appreciate your commitment to open science and your contributions to the research community.")
+ .append(NL2).append("Best regards,")
+ .append(NL).append(getUserName(journalAdmin));
+ return message.toString();
+ }
+
// The following link placeholders can be used in messages posted through the Panorama Public admin console (PostPanoramaPublicMessageAction).
// An example message (Markdown format):
/*
@@ -449,9 +578,13 @@ private static String escape(String text)
{
// https://www.markdownguide.org/basic-syntax/#characters-you-can-escape
// Escape Markdown special characters. Some character combinations can result in
- // unintended Markdown styling, e.g. "+_Italics_+" will results in "Italics" to be italicized.
+ // unintended Markdown styling, e.g. "+_Italics_+" results in "Italics" to be italicized.
// This can be seen with the tricky characters used for project names in labkey tests.
- return text.replaceAll("([`*_{}\\[\\]()#+.!|-])", "\\\\$1");
+ // 8/13/25 - Escape tilde (~) as well. In the LabKey Markdown flavor, text between
+ // single tildes (e.g., ~strikethrough~) is rendered as strikethrough.
+ // IMPORTANT: The dash (-) must be escaped in the regex or placed at the start/end of the
+ // character class to be treated as a literal dash rather than a range operator.
+ return text.replaceAll("([`*_{}\\[\\]()#+.!|~-])", "\\\\$1");
}
public static String getExperimentCopiedMessageBody(ExperimentAnnotations sourceExperiment,
@@ -572,8 +705,24 @@ public static class TestCase extends Assert
public void testMarkdownEscape()
{
Assert.assertEquals("\\+\\_Test\\_\\+", escape("+_Test_+"));
- Assert.assertEquals("PanoramaPublicTest Project ☃~\\!@$&\\(\\)\\_\\+\\{\\}\\-=\\[\\],\\.\\#äöüÅ",
- escape("PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ"));
+ String expected = "PanoramaPublicTest Project ☃\\~\\!@$&\\(\\)\\_\\+\\{\\}\\-=\\[\\],\\.\\#äöüÅ";
+ String escaped = escape("PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ");
+ Assert.assertEquals(expected, escaped);
+
+ /*
+ PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ This is a test PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ
+
+ should be translated to
+
+ PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ This is a test PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ
+
+ */
+ MarkdownService mds = MarkdownService.get();
+ expected = """
+ PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ This is a test PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ
+
""";
+ String testText = "PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ This is a test PanoramaPublicTest Project ☃~!@$&()_+{}-=[],.#äöüÅ";
+ Assert.assertEquals(expected, mds.toHtml(escape(testText)));
}
}
}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java
index ed731a4f..c40c0826 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java
+++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java
@@ -52,6 +52,7 @@
import org.labkey.panoramapublic.query.CatalogEntryTableInfo;
import org.labkey.panoramapublic.query.ContainerJoin;
import org.labkey.panoramapublic.query.DataValidationTableInfo;
+import org.labkey.panoramapublic.query.DatasetStatusTableInfo;
import org.labkey.panoramapublic.query.ExperimentAnnotationsTableInfo;
import org.labkey.panoramapublic.query.JournalExperimentTableInfo;
import org.labkey.panoramapublic.query.MyDataTableInfo;
@@ -95,6 +96,7 @@ public class PanoramaPublicSchema extends UserSchema
public static final String TABLE_LIB_SOURCE_TYPE = "SpecLibSourceType";
public static final String TABLE_CATALOG_ENTRY = "CatalogEntry";
+ public static final String TABLE_DATASET_STATUS = "DatasetStatus";
public PanoramaPublicSchema(User user, Container container)
{
@@ -317,6 +319,11 @@ public TableInfo createTable(String name, ContainerFilter cf)
return new CatalogEntryTableInfo(this, cf);
}
+ if (TABLE_DATASET_STATUS.equalsIgnoreCase(name))
+ {
+ return new DatasetStatusTableInfo(this, cf);
+ }
+
return null;
}
@@ -440,6 +447,7 @@ public Set getTableNames()
hs.add(TABLE_EXPT_STRUCTURAL_MOD_INFO);
hs.add(TABLE_EXPT_ISOTOPE_MOD_INFO);
hs.add(TABLE_CATALOG_ENTRY);
+ hs.add(TABLE_DATASET_STATUS);
return hs;
}
}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java
new file mode 100644
index 00000000..90101344
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java
@@ -0,0 +1,128 @@
+package org.labkey.panoramapublic.message;
+
+import org.apache.logging.log4j.Logger;
+import org.labkey.api.data.Container;
+import org.labkey.api.pipeline.PipeRoot;
+import org.labkey.api.pipeline.PipelineJob;
+import org.labkey.api.pipeline.PipelineService;
+import org.labkey.api.security.User;
+import org.labkey.api.util.ConfigurationException;
+import org.labkey.api.util.logging.LogHelper;
+import org.labkey.api.view.ViewBackgroundInfo;
+import org.labkey.panoramapublic.model.Journal;
+import org.labkey.panoramapublic.pipeline.PrivateDataReminderJob;
+import org.labkey.panoramapublic.query.JournalManager;
+import org.quartz.CronScheduleBuilder;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.JobExecutionContext;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.Trigger;
+import org.quartz.TriggerBuilder;
+import org.quartz.TriggerKey;
+import org.quartz.impl.StdSchedulerFactory;
+
+import java.time.LocalTime;
+
+public class PrivateDataMessageScheduler
+{
+ private static final Logger _log = LogHelper.getLogger(PrivateDataMessageScheduler.class, "Panorama Public private data reminder message scheduler");
+
+ private static final TriggerKey TRIGGER_KEY = new TriggerKey(PrivateDataMessageScheduler.class.getCanonicalName());
+
+ private static final PrivateDataMessageScheduler _instance = new PrivateDataMessageScheduler();
+
+ public static PrivateDataMessageScheduler getInstance()
+ {
+ return _instance;
+ }
+
+ private PrivateDataMessageScheduler(){}
+
+ public void initialize(boolean enable)
+ {
+ try
+ {
+ Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
+
+ // Clear previous job, if present
+ if (scheduler.checkExists(TRIGGER_KEY))
+ scheduler.unscheduleJob(TRIGGER_KEY);
+
+ if (!enable)
+ {
+ return;
+ }
+
+ PrivateDataReminderSettings settings = PrivateDataReminderSettings.get();
+ // Get the quartz Trigger
+ Trigger trigger = getTrigger(settings);
+
+ // Create a quartz job that queues a pipeline job that posts private data reminder messages
+ JobDetail job = JobBuilder.newJob(PrivateDataMessageSchedulerJob.class)
+ .withIdentity(PrivateDataMessageScheduler.class.getCanonicalName())
+ .build();
+
+ // Schedule trigger to send reminders on the configured schedule
+ scheduler.scheduleJob(job, trigger);
+ }
+ catch (SchedulerException e)
+ {
+ throw new RuntimeException("Failed to schedule PrivateDataMessageScheduler job", e);
+ }
+ }
+
+ protected Trigger getTrigger(PrivateDataReminderSettings settings)
+ {
+ LocalTime reminderTime = settings.getReminderTime();
+
+ // Runs every day at the specified time
+ return TriggerBuilder.newTrigger()
+ .withIdentity(TRIGGER_KEY)
+ .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(reminderTime.getHour(), reminderTime.getMinute()))
+ .build();
+ }
+
+ public static class PrivateDataMessageSchedulerJob implements Job
+ {
+ @SuppressWarnings("unused")
+ public PrivateDataMessageSchedulerJob() {}
+
+ @Override
+ public void execute(JobExecutionContext context)
+ {
+ try
+ {
+ Journal panoramaPublic = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC);
+ if (panoramaPublic == null)
+ {
+ throw new ConfigurationException("Server does not have a Panorama Public project.");
+ }
+
+ Container c = panoramaPublic.getProject();
+
+ User panoramaPublicAdmin = JournalManager.getJournalAdminUser(panoramaPublic);
+ if (panoramaPublicAdmin == null)
+ {
+ throw new ConfigurationException("Unable to find an admin user in the Panorama Public project.");
+ }
+ ViewBackgroundInfo vbi = new ViewBackgroundInfo(c, panoramaPublicAdmin, null);
+ PipeRoot root = PipelineService.get().findPipelineRoot(c);
+
+ if (root == null || !root.isValid())
+ {
+ throw new ConfigurationException("No valid pipeline root found in the container " + c.getName());
+ }
+
+ PipelineJob job = new PrivateDataReminderJob(vbi, PipelineService.get().getPipelineRootSetting(c), false);
+ PipelineService.get().queueJob(job);
+ }
+ catch(Exception e)
+ {
+ _log.error("Error queuing PrivateDataReminderJob", e);
+ }
+ }
+ }
+}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java
new file mode 100644
index 00000000..ee45663c
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java
@@ -0,0 +1,421 @@
+package org.labkey.panoramapublic.message;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Assert;
+import org.junit.Test;
+import org.labkey.api.data.PropertyManager;
+import org.labkey.api.util.DateUtil;
+import org.labkey.panoramapublic.model.DatasetStatus;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Date;
+
+public class PrivateDataReminderSettings
+{
+ public static final String PROP_PRIVATE_DATA_REMINDER = "Panorama Public private data reminder settings";
+ public static final String PROP_ENABLE_REMINDER = "Enable private data reminder";
+ public static final String PROP_REMINDER_TIME = "Reminder time";
+ public static final String PROP_DELAY_UNTIL_FIRST_REMINDER = "Delay until first reminder (months)";
+ public static final String PROP_REMINDER_FREQUENCY = "Reminder frequency (months)";
+ public static final String PROP_EXTENSION_LENGTH = "Extension duration (months)";
+
+ private static final boolean DEFAULT_ENABLE_REMINDERS = false;
+ public static final String DEFAULT_REMINDER_TIME = "8:00 AM";
+ private static final int DEFAULT_DELAY_UNTIL_FIRST_REMINDER = 12; // Send the first reminder after the data has been private for a year.
+ private static final int DEFAULT_REMINDER_FREQUENCY = 1; // Send reminders once a month, unless extension or deletion was requested.
+ private static final int DEFAULT_EXTENSION_LENGTH = 6; // Private status of a dataset can be extended by 6 months.
+
+ public static final String DATE_FORMAT_PATTERN = "MMMM d, yyyy";
+ public static final String REMINDER_TIME_FORMAT = "h:mm a";
+ private static final DateTimeFormatter reminderTimeFormatter = DateTimeFormatter.ofPattern(REMINDER_TIME_FORMAT);
+
+ private boolean _enableReminders;
+ private LocalTime _reminderTime;
+ private int _delayUntilFirstReminder;
+ private int _reminderFrequency;
+ private int _extensionLength;
+
+ public static PrivateDataReminderSettings get()
+ {
+ PropertyManager.WritablePropertyMap settingsMap = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, false);
+
+ PrivateDataReminderSettings settings = new PrivateDataReminderSettings();
+ if(settingsMap != null)
+ {
+ boolean enableReminders = settingsMap.get(PROP_ENABLE_REMINDER) == null
+ ? DEFAULT_ENABLE_REMINDERS
+ : Boolean.valueOf(settingsMap.get(PROP_ENABLE_REMINDER));
+ settings.setEnableReminders(enableReminders);
+
+ int delayUntilFirstReminder = settingsMap.get(PROP_DELAY_UNTIL_FIRST_REMINDER) == null
+ ? DEFAULT_DELAY_UNTIL_FIRST_REMINDER
+ : Integer.valueOf(settingsMap.get(PROP_DELAY_UNTIL_FIRST_REMINDER));
+ settings.setDelayUntilFirstReminder(delayUntilFirstReminder);
+
+ int reminderFrequency = settingsMap.get(PROP_REMINDER_FREQUENCY) == null
+ ? DEFAULT_REMINDER_FREQUENCY
+ : Integer.valueOf(settingsMap.get(PROP_REMINDER_FREQUENCY));
+ settings.setReminderFrequency(reminderFrequency);
+
+ int extensionLength = settingsMap.get(PROP_EXTENSION_LENGTH) == null
+ ? DEFAULT_EXTENSION_LENGTH
+ : Integer.valueOf(settingsMap.get(PROP_EXTENSION_LENGTH));
+ settings.setExtensionLength(extensionLength);
+
+ LocalTime reminderTime = tryParseReminderTime(settingsMap.get(PROP_REMINDER_TIME), DEFAULT_REMINDER_TIME);
+ settings.setReminderTime(reminderTime);
+ }
+ else
+ {
+ settings.setEnableReminders(DEFAULT_ENABLE_REMINDERS);
+ settings.setDelayUntilFirstReminder(DEFAULT_DELAY_UNTIL_FIRST_REMINDER);
+ settings.setReminderFrequency(DEFAULT_REMINDER_FREQUENCY);
+ settings.setExtensionLength(DEFAULT_EXTENSION_LENGTH);
+ settings.setReminderTime(parseReminderTime(DEFAULT_REMINDER_TIME));
+ }
+
+ return settings;
+ }
+
+ private static LocalTime tryParseReminderTime(String timeString, String defaultTime)
+ {
+ LocalTime reminderTime = parseReminderTime(timeString);
+ if (reminderTime == null)
+ {
+ reminderTime = parseReminderTime(defaultTime);
+ }
+ return reminderTime;
+ }
+
+ public static @Nullable LocalTime parseReminderTime(String timeString)
+ {
+ try
+ {
+ return timeString != null ? LocalTime.parse(timeString, reminderTimeFormatter) : null;
+ }
+ catch(DateTimeParseException ignored) {}
+
+ return null;
+ }
+
+ public static void save(PrivateDataReminderSettings settings)
+ {
+ PropertyManager.WritablePropertyMap settingsMap = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, true);
+ settingsMap.put(PROP_ENABLE_REMINDER, String.valueOf(settings.isEnableReminders()));
+ settingsMap.put(PROP_DELAY_UNTIL_FIRST_REMINDER, String.valueOf(settings.getDelayUntilFirstReminder()));
+ settingsMap.put(PROP_REMINDER_FREQUENCY, String.valueOf(settings.getReminderFrequency()));
+ settingsMap.put(PROP_EXTENSION_LENGTH, String.valueOf(settings.getExtensionLength()));
+ settingsMap.put(PROP_REMINDER_TIME, settings.getReminderTimeFormatted());
+ settingsMap.save();
+ }
+
+ public void setEnableReminders(boolean enableReminders)
+ {
+ _enableReminders = enableReminders;
+ }
+
+ public void setReminderTime(LocalTime reminderTime)
+ {
+ _reminderTime = reminderTime;
+ }
+
+ public void setExtensionLength(int extensionLength)
+ {
+ _extensionLength = extensionLength;
+ }
+
+ public void setReminderFrequency(int reminderFrequency)
+ {
+ _reminderFrequency = reminderFrequency;
+ }
+
+ public boolean isEnableReminders()
+ {
+ return _enableReminders;
+ }
+
+ public LocalTime getReminderTime()
+ {
+ return _reminderTime;
+ }
+
+ public String getReminderTimeFormatted()
+ {
+ return _reminderTime != null ? _reminderTime.format(reminderTimeFormatter) : "Reminder time not set";
+ }
+
+ public int getExtensionLength()
+ {
+ return _extensionLength;
+ }
+
+ public int getReminderFrequency()
+ {
+ return _reminderFrequency;
+ }
+
+ public int getDelayUntilFirstReminder()
+ {
+ return _delayUntilFirstReminder;
+ }
+
+ public void setDelayUntilFirstReminder(int delayUntilFirstReminder)
+ {
+ _delayUntilFirstReminder = delayUntilFirstReminder;
+ }
+
+ public @Nullable Date getReminderValidUntilDate(@NotNull DatasetStatus status)
+ {
+ return status.getLastReminderDate() == null ? null : addMonths(status.getLastReminderDate(), getReminderFrequency());
+ }
+
+ public boolean isLastReminderRecent(@NotNull DatasetStatus status)
+ {
+ return isDateInFuture(getReminderValidUntilDate(status));
+ }
+
+ public @Nullable Date getExtensionValidUntilDate(@NotNull DatasetStatus status)
+ {
+ return status.getExtensionRequestedDate() == null ? null : addMonths(status.getExtensionRequestedDate(), getExtensionLength());
+ }
+
+ public boolean isExtensionValid(@NotNull DatasetStatus status)
+ {
+ return isDateInFuture(getExtensionValidUntilDate(status));
+ }
+
+ public @Nullable String extensionValidUntilFormatted(@NotNull DatasetStatus status)
+ {
+ Date date = getExtensionValidUntilDate(status);
+ return date != null ? format(date) : null;
+ }
+
+ public static String format(@NotNull Date date)
+ {
+ return DateUtil.formatDateTime(date, DATE_FORMAT_PATTERN);
+ }
+
+ private static boolean isDateInFuture(@Nullable Date date)
+ {
+ return isDateInFuture(date, new Date());
+ }
+
+ private static boolean isDateInFuture(@Nullable Date date, @NotNull Date currentTime)
+ {
+ return date != null && date.after(currentTime);
+ }
+
+ private static Date addMonths(@NotNull Date date, int months)
+ {
+ return Date.from(dateToZonedDateTime(date).plusMonths(months).toInstant());
+ }
+
+ private static ZonedDateTime dateToZonedDateTime(@NotNull Date date)
+ {
+ return date.toInstant().atZone(ZoneId.systemDefault());
+ }
+
+ private boolean isExtensionValidAsOf(@NotNull DatasetStatus status, @NotNull Date currentTime)
+ {
+ Date extensionValidUntil = getExtensionValidUntilDate(status);
+ return isDateInFuture(extensionValidUntil, currentTime);
+ }
+
+ private boolean isLastReminderRecentAsOf(@NotNull DatasetStatus status, @NotNull Date currentTime)
+ {
+ Date reminderValidUntil = getReminderValidUntilDate(status);
+ return isDateInFuture(reminderValidUntil, currentTime);
+ }
+
+ public static class TestCase extends Assert
+ {
+ @Test
+ public void testIsExtensionCurrentScenarios()
+ {
+ DatasetStatus datasetStatus = new DatasetStatus();
+ PrivateDataReminderSettings settings = createTestSettingsExtensionLength(6);
+
+ // No extension requested (extensionRequestDate is null)
+ assertFalse("Should return false; extensionRequestDate is null", settings.isExtensionValid(datasetStatus));
+
+ // Extension request is within the configured extension period
+ testExtensionIsValid(settings, -3);
+
+ // Extension request has expired
+ testExtensionIsExpired(settings, -7);
+
+ // Extension request expires exactly now, not in the future.
+ testExtensionIsExpiredAsOf(settings, (settings.getExtensionLength() * -1), 0);
+
+ // Extension expires in 1 minute - still current
+ testExtensionIsValidAsOf(settings, (settings.getExtensionLength() * -1), 1);
+
+ // Extension expired 1 minute ago
+ testExtensionIsExpiredAsOf(settings, (settings.getExtensionLength() * -1), -1);
+ }
+
+ @Test
+ public void testIsLastReminderRecentScenarios()
+ {
+ DatasetStatus datasetStatus = new DatasetStatus();
+ PrivateDataReminderSettings settings = createTestSettingsReminderFrequency(2);
+
+ // No reminder sent yet (lastReminderDate is null)
+ assertFalse("Should return false; lastReminderDate is null", settings.isLastReminderRecent(datasetStatus));
+
+ // Last reminder sent within the reminder frequency period
+ testReminderIsRecent(settings, -15);
+
+ // Reminder is old
+ testReminderIsOld(settings, -70);
+
+ // Reminder is old now
+ testReminderIsOldAsOf(settings, (settings.getReminderFrequency() * -1), 0);
+
+ // Reminder gets old in 1 minute - still current
+ testReminderIsRecentAsOf(settings, (settings.getReminderFrequency() * -1), 1);
+
+ // Reminder became old 1 minute ago
+ testReminderIsOldAsOf(settings, (settings.getReminderFrequency() * -1), -1);
+
+ // Setting the reminder frequency to 0 will return false for isReminderRecent, unless lastReminderDate is set in the future.
+ settings = createTestSettingsReminderFrequency(0);
+ datasetStatus = new DatasetStatus();
+ assertFalse("Should return false; lastReminderDate is null", settings.isLastReminderRecent(datasetStatus));
+ testReminderIsOld(settings, -15);
+ testReminderIsOldAsOf(settings, (settings.getReminderFrequency() * -1), 0);
+ testReminderIsRecent(settings, 30); // Reminder date is set in the future
+ }
+
+ private void testExtensionIsValid(PrivateDataReminderSettings settings, int monthsOffset)
+ {
+ testExtensionIsValid(settings, monthsOffset, 0, null, true);
+ }
+
+ private void testExtensionIsExpired(PrivateDataReminderSettings settings, int monthsOffset)
+ {
+ testExtensionIsValid(settings, monthsOffset, 0, null, false);
+ }
+
+ private void testExtensionIsValidAsOf(PrivateDataReminderSettings settings, int monthsOffset, int minutesOffset)
+ {
+ testExtensionIsValid(settings, monthsOffset, minutesOffset, dateFromNow(), true);
+ }
+
+ private void testExtensionIsExpiredAsOf(PrivateDataReminderSettings settings, int monthsOffset, int minutesOffset)
+ {
+ testExtensionIsValid(settings, monthsOffset, minutesOffset, dateFromNow(), false);
+ }
+
+ private void testExtensionIsValid(PrivateDataReminderSettings settings, int monthsOffset, int minutesOffset,
+ Date currentDate, boolean expectedValid)
+ {
+ Date extensionDate = dateFromNow(monthsOffset, 0, minutesOffset);
+
+ DatasetStatus datasetStatus = new DatasetStatus();
+ datasetStatus.setExtensionRequestedDate(extensionDate);
+
+ String failureMessage = String.format("Extension is %s; Extension Length: %d; Extension Requested On: %s; Valid Until: %s",
+ expectedValid ? "valid" : "expired",
+ settings.getExtensionLength(),
+ datasetStatus.getExtensionRequestedDate(),
+ settings.getExtensionValidUntilDate(datasetStatus));
+ if (currentDate != null)
+ {
+ failureMessage += String.format("; Current Date: %s", currentDate);
+ }
+
+ boolean isValid = currentDate == null
+ ? settings.isExtensionValid(datasetStatus)
+ : settings.isExtensionValidAsOf(datasetStatus, currentDate);
+ if (expectedValid) assertTrue(failureMessage, isValid);
+ else assertFalse(failureMessage, isValid);
+ }
+
+ private void testReminderIsRecent(PrivateDataReminderSettings settings, int daysOffset)
+ {
+ testReminderIsRecent(settings, 0, daysOffset, 0, null, true);
+ }
+
+ private void testReminderIsOld(PrivateDataReminderSettings settings, int daysOffset)
+ {
+ testReminderIsRecent(settings, 0, daysOffset, 0, null, false);
+ }
+
+ private void testReminderIsRecentAsOf(PrivateDataReminderSettings settings, int monthsOffset, int minutesOffset)
+ {
+ testReminderIsRecent(settings, monthsOffset, 0, minutesOffset, dateFromNow(), true);
+ }
+
+ private void testReminderIsOldAsOf(PrivateDataReminderSettings settings, int monthsOffset, int minutesOffset)
+ {
+ testReminderIsRecent(settings, monthsOffset, 0, minutesOffset, dateFromNow(), false);
+ }
+
+ private void testReminderIsRecent(PrivateDataReminderSettings settings, int monthsOffset, int daysOffset, int minutesOffset,
+ Date currentDate, boolean expectedRecent)
+ {
+ Date reminderDate = dateFromNow(monthsOffset, daysOffset, minutesOffset);
+
+ DatasetStatus datasetStatus = new DatasetStatus();
+ datasetStatus.setLastReminderDate(reminderDate);
+
+ String failureMessage = String.format("Reminder is %s; Reminder Frequency: %d; Reminder Sent On: %s; Valid Until: %s",
+ expectedRecent ? "recent" : "old",
+ settings.getReminderFrequency(),
+ datasetStatus.getLastReminderDate(),
+ settings.getReminderValidUntilDate(datasetStatus));
+ if (currentDate != null)
+ {
+ failureMessage += String.format("; Current Date: %s", currentDate);
+ }
+
+ boolean isValid = currentDate == null
+ ? settings.isLastReminderRecent(datasetStatus)
+ : settings.isLastReminderRecentAsOf(datasetStatus, currentDate);
+ if (expectedRecent) assertTrue(failureMessage, isValid);
+ else assertFalse(failureMessage, isValid);
+ }
+
+ private PrivateDataReminderSettings createTestSettingsExtensionLength(int extensionLength)
+ {
+ return createTestSettings(extensionLength, 0);
+ }
+
+ private PrivateDataReminderSettings createTestSettingsReminderFrequency(int reminderFrequency)
+ {
+ return createTestSettings(0, reminderFrequency);
+ }
+
+ private PrivateDataReminderSettings createTestSettings(int extensionLength, int reminderFrequency)
+ {
+ PrivateDataReminderSettings testSettings = new PrivateDataReminderSettings();
+ testSettings.setExtensionLength(extensionLength);
+ testSettings.setReminderFrequency(reminderFrequency);
+ return testSettings;
+ }
+
+ private Date dateFromNow()
+ {
+ return dateFromNow(0, 0, 0);
+ }
+
+ private Date dateFromNow(int monthsOffset, int daysOffset, int minutesOffset)
+ {
+ return Date.from(
+ LocalDate.now()
+ .plusMonths(monthsOffset)
+ .plusDays(daysOffset)
+ .atStartOfDay(ZoneId.systemDefault())
+ .plusMinutes(minutesOffset)
+ .toInstant()
+ );
+ }
+ }
+}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java
new file mode 100644
index 00000000..717da27d
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java
@@ -0,0 +1,75 @@
+package org.labkey.panoramapublic.model;
+
+import org.jetbrains.annotations.Nullable;
+import org.labkey.api.view.ShortURLRecord;
+import org.labkey.panoramapublic.message.PrivateDataReminderSettings;
+
+import java.util.Date;
+
+public class DatasetStatus extends DbEntity
+{
+ private int _experimentAnnotationsId;
+ private Date _lastReminderDate;
+ private Date _extensionRequestedDate;
+ private Date _deletionRequestedDate;
+
+ public int getExperimentAnnotationsId()
+ {
+ return _experimentAnnotationsId;
+ }
+
+ public void setExperimentAnnotationsId(int experimentAnnotationsId)
+ {
+ _experimentAnnotationsId = experimentAnnotationsId;
+ }
+
+ public Date getLastReminderDate()
+ {
+ return _lastReminderDate;
+ }
+
+ public void setLastReminderDate(Date lastReminderDate)
+ {
+ _lastReminderDate = lastReminderDate;
+ }
+
+ public Date getExtensionRequestedDate()
+ {
+ return _extensionRequestedDate;
+ }
+
+ public void setExtensionRequestedDate(Date extensionRequestedDate)
+ {
+ _extensionRequestedDate = extensionRequestedDate;
+ }
+
+ public Date getDeletionRequestedDate()
+ {
+ return _deletionRequestedDate;
+ }
+
+ public @Nullable String getDeletionRequestedDateFormatted()
+ {
+ return PrivateDataReminderSettings.format(_deletionRequestedDate);
+ }
+
+ public void setDeletionRequestedDate(Date deletionRequestedDate)
+ {
+ _deletionRequestedDate = deletionRequestedDate;
+ }
+
+ public boolean deletionRequested()
+ {
+ return _deletionRequestedDate != null;
+ }
+
+ public boolean extensionRequested()
+ {
+ return _extensionRequestedDate != null;
+ }
+
+ public boolean reminderSent()
+ {
+ return _lastReminderDate != null;
+ }
+}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java
index cb2c0138..362f450e 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java
+++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java
@@ -74,6 +74,7 @@
import org.labkey.panoramapublic.proteomexchange.ProteomeXchangeService;
import org.labkey.panoramapublic.proteomexchange.ProteomeXchangeServiceException;
import org.labkey.panoramapublic.query.CatalogEntryManager;
+import org.labkey.panoramapublic.query.DatasetStatusManager;
import org.labkey.panoramapublic.query.ExperimentAnnotationsManager;
import org.labkey.panoramapublic.query.JournalManager;
import org.labkey.panoramapublic.query.SubmissionManager;
diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java
new file mode 100644
index 00000000..2b5bfe6b
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java
@@ -0,0 +1,627 @@
+package org.labkey.panoramapublic.pipeline;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.labkey.api.announcements.api.Announcement;
+import org.labkey.api.announcements.api.AnnouncementService;
+import org.labkey.api.data.Container;
+import org.labkey.api.data.ContainerManager;
+import org.labkey.api.data.DbScope;
+import org.labkey.api.pipeline.PipeRoot;
+import org.labkey.api.pipeline.PipelineJob;
+import org.labkey.api.portal.ProjectUrls;
+import org.labkey.api.security.User;
+import org.labkey.api.util.FileUtil;
+import org.labkey.api.util.PageFlowUtil;
+import org.labkey.api.util.StringUtilsLabKey;
+import org.labkey.api.util.URLHelper;
+import org.labkey.api.view.ViewBackgroundInfo;
+import org.labkey.panoramapublic.PanoramaPublicManager;
+import org.labkey.panoramapublic.PanoramaPublicNotification;
+import org.labkey.panoramapublic.message.PrivateDataReminderSettings;
+import org.labkey.panoramapublic.model.DatasetStatus;
+import org.labkey.panoramapublic.model.ExperimentAnnotations;
+import org.labkey.panoramapublic.model.Journal;
+import org.labkey.panoramapublic.model.JournalSubmission;
+import org.labkey.panoramapublic.query.DatasetStatusManager;
+import org.labkey.panoramapublic.query.ExperimentAnnotationsManager;
+import org.labkey.panoramapublic.query.JournalManager;
+import org.labkey.panoramapublic.query.SubmissionManager;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class PrivateDataReminderJob extends PipelineJob
+{
+ private boolean _test;
+ private List _experimentAnnotationsIds;
+ private Journal _panoramaPublic;
+
+ protected PrivateDataReminderJob()
+ {
+ }
+
+ public PrivateDataReminderJob(ViewBackgroundInfo info, @NotNull PipeRoot root, boolean test)
+ {
+ this(info, root, getPanoramaPublic(), getPrivateDatasets(getPanoramaPublic()), test);
+ }
+
+ public PrivateDataReminderJob(ViewBackgroundInfo info, @NotNull PipeRoot root, Journal panoramaPublic, List experimentAnnotationsIds, boolean test)
+ {
+ super("Panorama Public", info, root);
+ setLogFile(root.getRootFileLike().toNioPathForWrite().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-private-data-reminder", "log")));
+ _panoramaPublic = panoramaPublic;
+
+ _experimentAnnotationsIds = experimentAnnotationsIds;
+ _test = test;
+ }
+
+ private static Journal getPanoramaPublic()
+ {
+ return JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC);
+ }
+
+ public static List getPrivateDatasets(Journal panoramaPublic)
+ {
+ if (panoramaPublic == null) return Collections.emptyList();
+
+ Set subFolders = ContainerManager.getAllChildren(panoramaPublic.getProject());
+ List privateDataIds = new ArrayList<>();
+ for (Container folder : subFolders)
+ {
+ ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder);
+ if (exptAnnotations != null && !exptAnnotations.isPublic())
+ {
+ privateDataIds.add(exptAnnotations.getId());
+ }
+ }
+
+ return privateDataIds;
+ }
+
+ private static ReminderDecision getReminderDecision(@NotNull ExperimentAnnotations exptAnnotations, @NotNull PrivateDataReminderSettings settings)
+ {
+ if (exptAnnotations.isPublic())
+ {
+ return ReminderDecision.skip("Data is already public");
+ }
+
+ if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations))
+ {
+ return ReminderDecision.skip("Not the current version of the experiment");
+ }
+
+ DatasetStatus datasetStatus = DatasetStatusManager.getForExperiment(exptAnnotations);
+ if (datasetStatus != null)
+ {
+ if (datasetStatus.deletionRequested())
+ {
+ return ReminderDecision.skip("Submitter has requested deletion");
+ }
+
+ if (settings.isExtensionValid(datasetStatus))
+ {
+ return ReminderDecision.skip("Submitter requested an extension. Extension is current");
+ }
+
+ if (settings.isLastReminderRecent(datasetStatus))
+ {
+ return ReminderDecision.skip("Recent reminder already sent");
+ }
+ }
+ return reminderIsDue(exptAnnotations, settings);
+ }
+
+ private static ReminderDecision reminderIsDue(ExperimentAnnotations exptAnnotations, PrivateDataReminderSettings settings)
+ {
+ LocalDate copyDate = exptAnnotations.getCreated().toInstant()
+ .atZone(ZoneId.systemDefault())
+ .toLocalDate();
+
+ LocalDate firstReminderDate = copyDate.plusMonths(settings.getDelayUntilFirstReminder());
+ if (LocalDate.now().isBefore(firstReminderDate))
+ {
+ return ReminderDecision.skip(String.format("First reminder not due until %s",
+ firstReminderDate.format(DateTimeFormatter.ofPattern(PrivateDataReminderSettings.DATE_FORMAT_PATTERN))));
+ }
+ return ReminderDecision.post();
+ }
+
+ public static class ReminderDecision {
+ private final boolean shouldPost;
+ private final String reason;
+
+ private ReminderDecision(boolean shouldPost, String reason) {
+ this.shouldPost = shouldPost;
+ this.reason = reason;
+ }
+
+ public static ReminderDecision post() {
+ return new ReminderDecision(true, null);
+ }
+
+ public static ReminderDecision skip(String reason) {
+ return new ReminderDecision(false, reason);
+ }
+
+ public boolean shouldPost() { return shouldPost; }
+ public String getReason() { return reason; }
+ }
+
+ @Override
+ public void run()
+ {
+ setStatus(TaskStatus.running);
+
+ if (_panoramaPublic == null)
+ {
+ getLogger().error("Panorama Public project does not exist.");
+ return;
+ }
+
+ postMessage(_experimentAnnotationsIds, _panoramaPublic);
+
+ setStatus(TaskStatus.complete);
+ }
+
+ private void postMessage(List expAnnotationIds, Journal panoramaPublic)
+ {
+ int total = expAnnotationIds.size();
+ if (total == 0)
+ {
+ getLogger().info("No private datasets were found.");
+ return;
+ }
+ Logger log = getLogger();
+
+ ProcessingContext context = ProcessingContext.create(panoramaPublic, getUser(), _test);
+ if(!context.isValid())
+ {
+ context.logErrors(log);
+ return;
+ }
+ ProcessingResults processingResults = new ProcessingResults(expAnnotationIds.size(), log);
+
+ processExperiments(_experimentAnnotationsIds, context, processingResults, log);
+
+ }
+
+ private void processExperiments(List expAnnotationIds, ProcessingContext context, ProcessingResults processingResults, Logger log)
+ {
+ log.info(String.format("Posting reminder message to: %d message threads.", expAnnotationIds.size()));
+
+ Set exptIds = new HashSet<>(expAnnotationIds);
+ try (DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction())
+ {
+ if (_test)
+ {
+ log.info("RUNNING IN TEST MODE - MESSAGES WILL NOT BE POSTED.");
+ }
+ for (Integer experimentAnnotationsId : exptIds)
+ {
+ processExperiment(experimentAnnotationsId, context, processingResults);
+ }
+ transaction.commit();
+ }
+
+ processingResults.logResults(log);
+ }
+
+ private void processExperiment(Integer experimentAnnotationsId, ProcessingContext context, ProcessingResults processingResults)
+ {
+ ExperimentAnnotations expAnnotations = ExperimentAnnotationsManager.get(experimentAnnotationsId);
+ if (expAnnotations == null)
+ {
+ processingResults.addExperimentNotFound(experimentAnnotationsId);
+ return;
+ }
+
+ ReminderDecision decision = getReminderDecision(expAnnotations, context.getSettings());
+ if (!decision.shouldPost())
+ {
+ processingResults.addSkipped(experimentAnnotationsId, decision);
+ return;
+ }
+ JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(expAnnotations);
+ if (submission == null)
+ {
+ processingResults.addSubmissionNotFound(experimentAnnotationsId);
+ return;
+ }
+ if (submission.getLatestSubmission() == null)
+ {
+ processingResults.addLatestSubmissionNotFound(experimentAnnotationsId);
+ return;
+ }
+
+ Container announcementsFolder = context.getAnnouncementsFolder();
+ Announcement announcement = context.getAnnouncementService().getAnnouncement(announcementsFolder, getUser(), submission.getAnnouncementId());
+ if (announcement == null)
+ {
+ processingResults.addAnnouncementNotFound(experimentAnnotationsId, submission, announcementsFolder);
+ return;
+ }
+
+ User submitter = expAnnotations.getSubmitterUser();
+ if (submitter == null)
+ {
+ processingResults.addSubmitterNotFound(experimentAnnotationsId);
+ return;
+ }
+
+ if (!context.isTestMode())
+ {
+ postReminderMessage(expAnnotations, submission, announcement, submitter, context);
+
+ updateDatasetStatus(expAnnotations);
+ }
+
+ processingResults.addProcessed(expAnnotations, announcement);
+ }
+
+ private void postReminderMessage(ExperimentAnnotations expAnnotations, JournalSubmission submission,
+ Announcement announcement, User submitter, ProcessingContext context)
+ {
+ // Older message threads, pre March 2023, will not have the submitter or lab head on the notify list. Add them.
+ List notifyList = new ArrayList<>();
+ notifyList.add(submitter);
+ if (expAnnotations.getLabHeadUser() != null) {
+ notifyList.add(expAnnotations.getLabHeadUser());
+ }
+
+ PanoramaPublicNotification.postPrivateDataReminderMessage(
+ context.getJournal(),
+ submission,
+ expAnnotations,
+ submitter,
+ context.getCurrentUser(),
+ notifyList,
+ announcement,
+ context.getAnnouncementsFolder(),
+ context.getJournalAdmin()
+ );
+ }
+
+ private void updateDatasetStatus(ExperimentAnnotations expAnnotations)
+ {
+ DatasetStatus datasetStatus = DatasetStatusManager.getForExperiment(expAnnotations);
+ if (datasetStatus == null)
+ {
+ datasetStatus = new DatasetStatus();
+ datasetStatus.setExperimentAnnotationsId(expAnnotations.getId());
+ datasetStatus.setLastReminderDate(new Date());
+ DatasetStatusManager.save(datasetStatus, getUser());
+ }
+ else
+ {
+ datasetStatus.setLastReminderDate(new Date());
+ DatasetStatusManager.update(datasetStatus, getUser());
+ }
+ }
+
+ @Override
+ public URLHelper getStatusHref()
+ {
+ return null;
+ }
+
+ @Override
+ public String getDescription()
+ {
+ return "Post private data reminder messages";
+ }
+
+ private static class ProcessingContext
+ {
+ private final PrivateDataReminderSettings _reminderSettings;
+ private final AnnouncementService _announcementService;
+ private final Container _announcementsFolder;
+ private final User _journalAdmin;
+ private final User _currentUser;
+ private final Journal _journal;
+ private final boolean _testMode;
+
+ private final List _errors;
+
+ private ProcessingContext(Builder builder)
+ {
+ _reminderSettings = builder.settings;
+ _announcementService = builder.announcementService;
+ _announcementsFolder = builder.announcementsFolder;
+ _journalAdmin = builder.journalAdmin;
+ _currentUser = builder.currentUser;
+ _journal = builder.journal;
+ _testMode = builder.testMode;
+ _errors = new ArrayList<>(builder.creationErrors);
+ }
+
+ public PrivateDataReminderSettings getSettings()
+ {
+ return _reminderSettings;
+ }
+ public AnnouncementService getAnnouncementService()
+ {
+ return _announcementService;
+ }
+ public Container getAnnouncementsFolder()
+ {
+ return _announcementsFolder;
+ }
+ public User getJournalAdmin()
+ {
+ return _journalAdmin;
+ }
+ public User getCurrentUser()
+ {
+ return _currentUser;
+ }
+ public Journal getJournal()
+ {
+ return _journal;
+ }
+ public boolean isTestMode()
+ {
+ return _testMode;
+ }
+
+ public boolean isValid()
+ {
+ return _reminderSettings != null &&
+ _announcementService != null &&
+ _announcementsFolder != null &&
+ _journalAdmin != null &&
+ _currentUser != null &&
+ _journal != null;
+ }
+
+ public void logErrors(Logger log)
+ {
+ for (String error: _errors)
+ {
+ log.error(error);
+ }
+ }
+
+ private static class Builder
+ {
+ private PrivateDataReminderSettings settings;
+ private AnnouncementService announcementService;
+ private Container announcementsFolder;
+ private User journalAdmin;
+ private User currentUser;
+ private Journal journal;
+ private boolean testMode = false;
+ private final List creationErrors = new ArrayList<>();
+
+ public Builder withSettings(@Nullable PrivateDataReminderSettings settings)
+ {
+ this.settings = settings;
+ if (settings == null)
+ {
+ creationErrors.add("Could not load PrivateDataReminderSettings.");
+ }
+ return this;
+ }
+
+ public Builder withAnnouncementService(@Nullable AnnouncementService announcementService)
+ {
+ this.announcementService = announcementService;
+ if (announcementService == null)
+ {
+ creationErrors.add("Could not get AnnouncementService.");
+ }
+ return this;
+ }
+
+ public Builder withAnnouncementsFolder(@Nullable Container announcementsFolder)
+ {
+ this.announcementsFolder = announcementsFolder;
+ if (announcementsFolder == null)
+ {
+ creationErrors.add("Announcements folder is null - Panorama Public project does not have a support folder for messages.");
+ }
+ return this;
+ }
+
+ public Builder withJournalAdmin(@Nullable User journalAdmin)
+ {
+ this.journalAdmin = journalAdmin;
+ if (journalAdmin == null)
+ {
+ creationErrors.add("Could not find an admin user for the Panorama Public project.");
+ }
+ return this;
+ }
+
+ public Builder withCurrentUser(@Nullable User currentUser)
+ {
+ this.currentUser = currentUser;
+ if (currentUser == null)
+ {
+ creationErrors.add("Current user is null.");
+ }
+ return this;
+ }
+
+ public Builder withJournal(@Nullable Journal journal)
+ {
+ this.journal = journal;
+ if (journal == null)
+ {
+ creationErrors.add("Panorama Public journal is null.");
+ }
+ return this;
+ }
+
+ public Builder withTestMode(boolean testMode)
+ {
+ this.testMode = testMode;
+ return this;
+ }
+
+ /**
+ * Adds a custom error message to the creation errors
+ */
+ public Builder addError(String errorMessage)
+ {
+ if (errorMessage != null && !errorMessage.trim().isEmpty())
+ {
+ creationErrors.add(errorMessage.trim());
+ }
+ return this;
+ }
+
+ public ProcessingContext build()
+ {
+ return new ProcessingContext(this);
+ }
+ }
+ public static ProcessingContext create(@NotNull Journal panoramaPublic, User user, boolean testMode)
+ {
+ Builder builder = new Builder()
+ .withJournal(panoramaPublic)
+ .withJournalAdmin(JournalManager.getJournalAdminUser(panoramaPublic))
+ .withAnnouncementsFolder(panoramaPublic.getSupportContainer())
+ .withCurrentUser(user)
+ .withTestMode(testMode)
+ .withSettings(PrivateDataReminderSettings.get())
+ .withAnnouncementService(AnnouncementService.get());
+
+ return builder.build();
+ }
+ }
+
+ private static class ProcessingResults
+ {
+ private final List _experimentNotFound = new ArrayList<>();
+ private final List _submissionNotFound = new ArrayList<>();
+ private final List _announcementNotFound = new ArrayList<>();
+ private final List _submitterNotFound = new ArrayList<>();
+ private final List _skipped = new ArrayList<>();
+ private int _processed = 0;
+ private final int _total;
+ private final Logger _log;
+
+ public ProcessingResults(int totalExperiments, Logger log)
+ {
+ _total = totalExperiments;
+ _log = log;
+ }
+
+ public void addExperimentNotFound(Integer experimentId)
+ {
+ _experimentNotFound.add(experimentId);
+ _log.error(String.format("Could not find an experiment with Id: %s.", experimentId));
+ }
+
+ public void addSubmissionNotFound(Integer experimentId)
+ {
+ _submissionNotFound.add(experimentId);
+ _log.error(String.format("Could not find a submission request for experiment Id: %s.", experimentId));
+ }
+ public void addLatestSubmissionNotFound(Integer experimentId)
+ {
+ _submissionNotFound.add(experimentId);
+ _log.error(String.format("Submission found but latest submission is null for experiment Id: %s.", experimentId));
+ }
+
+ public void addAnnouncementNotFound(Integer experimentId, JournalSubmission submission, Container announcementsFolder)
+ {
+ _announcementNotFound.add(experimentId);
+ _log.error(String.format("Could not find the message thread for experiment Id: %s; announcement Id: %s in the folder %s.",
+ experimentId, submission.getAnnouncementId(), announcementsFolder.getPath()));
+ }
+
+ public void addSubmitterNotFound(Integer experimentId)
+ {
+ _submitterNotFound.add(experimentId);
+ _log.error(String.format("Could not find a submitter user for experiment Id: %s.", experimentId));
+ }
+
+ public void addSkipped(Integer experimentId, ReminderDecision decision)
+ {
+ _skipped.add(experimentId);
+ _log.info(String.format("Skipping reminder for experiment Id %s - %s.", experimentId, decision.getReason()));
+ }
+
+ public void addProcessed(ExperimentAnnotations expAnnotations, Announcement announcement)
+ {
+ _processed++;
+ _log.info(String.format("Experiment ID: %d; Announcement ID %d; Short URL: %s.",
+ expAnnotations.getId(), announcement.getRowId(), expAnnotations.getShortUrl().renderShortURL()));
+ _log.info(String.format("Folder: %s", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(expAnnotations.getContainer()).getURIString()));
+ _log.info(String.format("Completed: %d of %d", _processed, _total));
+ }
+
+ public void logResults(Logger log)
+ {
+ logSkipped(log);
+ logSummary(log);
+ }
+
+ public void logSkipped(Logger log)
+ {
+ if (!_experimentNotFound.isEmpty())
+ {
+ log.error("Experiments with the following Ids could not be found: " +
+ StringUtils.join(_experimentNotFound, ", "));
+ }
+
+ if (!_submissionNotFound.isEmpty())
+ {
+ log.error("Submission requests were not found for the following experiment Ids: " +
+ StringUtils.join(_submissionNotFound, ", "));
+ }
+
+ if (!_announcementNotFound.isEmpty())
+ {
+ log.error("Support message threads were not found for the following experiment Ids: " +
+ StringUtils.join(_announcementNotFound, ", "));
+ }
+
+ if (!_submitterNotFound.isEmpty())
+ {
+ log.error("Submitter user was not found for the following experiment Ids: " +
+ StringUtils.join(_submitterNotFound, ", "));
+ }
+
+ if (!_skipped.isEmpty())
+ {
+ log.info("The following experiments were skipped: " +
+ StringUtils.join(_skipped, ", "));
+ }
+ }
+
+ public int getTotalErrors()
+ {
+ return _experimentNotFound.size() +
+ _submissionNotFound.size() +
+ _announcementNotFound.size() +
+ _submitterNotFound.size();
+ }
+ public void logSummary(Logger log)
+ {
+ if (!_skipped.isEmpty())
+ {
+ log.info(String.format("Skipped posting reminders for %s.", StringUtilsLabKey.pluralize(_skipped.size(), "experiment")));
+ }
+
+ if (_processed > 0)
+ {
+ log.info(String.format("Successfully processed %s.", StringUtilsLabKey.pluralize(_processed, "experiment")));
+ }
+
+ log.info(String.format("Processing complete: %d total, %d processed, %d skipped, %d errors",
+ _total, _processed, _skipped.size(), getTotalErrors()));
+ }
+ }
+}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java
new file mode 100644
index 00000000..c1c2ca5c
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java
@@ -0,0 +1,52 @@
+package org.labkey.panoramapublic.query;
+
+import org.jetbrains.annotations.Nullable;
+import org.labkey.api.data.DbScope;
+import org.labkey.api.data.SimpleFilter;
+import org.labkey.api.data.Table;
+import org.labkey.api.data.TableSelector;
+import org.labkey.api.query.FieldKey;
+import org.labkey.api.security.User;
+import org.labkey.panoramapublic.PanoramaPublicManager;
+import org.labkey.panoramapublic.model.DatasetStatus;
+import org.labkey.panoramapublic.model.ExperimentAnnotations;
+
+public class DatasetStatusManager
+{
+ public static DatasetStatus get(int datasetStatusId)
+ {
+ return new TableSelector(PanoramaPublicManager.getTableInfoDatasetStatus(),null, null).getObject(datasetStatusId, DatasetStatus.class);
+ }
+
+ public static @Nullable DatasetStatus getForExperiment(ExperimentAnnotations experimentAnnotations)
+ {
+ if (experimentAnnotations != null)
+ {
+ SimpleFilter filter = new SimpleFilter();
+ filter.addCondition(FieldKey.fromParts("ExperimentAnnotationsId"), experimentAnnotations.getId());
+ return new TableSelector(PanoramaPublicManager.getTableInfoDatasetStatus(), filter, null).getObject(DatasetStatus.class);
+ }
+ return null;
+ }
+
+ public static void save(DatasetStatus datasetStatus, User user)
+ {
+ Table.insert(user, PanoramaPublicManager.getTableInfoDatasetStatus(), datasetStatus);
+ }
+
+ public static void update(DatasetStatus datasetStatus, User user)
+ {
+ Table.update(user, PanoramaPublicManager.getTableInfoDatasetStatus(), datasetStatus, datasetStatus.getId());
+ }
+
+ public static void deleteStatusForExperiment(ExperimentAnnotations expAnnotations)
+ {
+ if (expAnnotations == null) return;
+
+ try(DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction())
+ {
+ Table.delete(PanoramaPublicManager.getTableInfoDatasetStatus(), new SimpleFilter(FieldKey.fromParts("ExperimentAnnotationsId"), expAnnotations.getId()));
+ transaction.commit();
+ }
+ }
+}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java
new file mode 100644
index 00000000..e0413edd
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java
@@ -0,0 +1,36 @@
+package org.labkey.panoramapublic.query;
+
+import org.jetbrains.annotations.NotNull;
+import org.labkey.api.data.ContainerFilter;
+import org.labkey.api.data.JdbcType;
+import org.labkey.api.data.SQLFragment;
+import org.labkey.api.query.ExprColumn;
+import org.labkey.api.query.FieldKey;
+import org.labkey.api.query.QueryForeignKey;
+import org.labkey.panoramapublic.PanoramaPublicManager;
+import org.labkey.panoramapublic.PanoramaPublicSchema;
+import org.labkey.panoramapublic.view.publish.ShortUrlDisplayColumnFactory;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DatasetStatusTableInfo extends PanoramaPublicTable
+{
+ public DatasetStatusTableInfo(@NotNull PanoramaPublicSchema userSchema, ContainerFilter cf)
+ {
+ super(PanoramaPublicManager.getTableInfoDatasetStatus(), userSchema, cf,
+ new ContainerJoin("ExperimentAnnotationsId", PanoramaPublicManager.getTableInfoExperimentAnnotations(), "Id"));
+
+ List visibleColumns = new ArrayList<>();
+ visibleColumns.add(FieldKey.fromParts("Created"));
+ visibleColumns.add(FieldKey.fromParts("CreatedBy"));
+ visibleColumns.add(FieldKey.fromParts("Modified"));
+ visibleColumns.add(FieldKey.fromParts("ModifiedBy"));
+ visibleColumns.add(FieldKey.fromParts("ExperimentAnnotationsId", "Link"));
+ visibleColumns.add(FieldKey.fromParts("ExperimentAnnotationsId", "Title"));
+ visibleColumns.add(FieldKey.fromParts("LastReminderDate"));
+ visibleColumns.add(FieldKey.fromParts("ExtensionRequestedDate"));
+ visibleColumns.add(FieldKey.fromParts("DeletionRequestedDate"));
+ setDefaultVisibleColumns(visibleColumns);
+ }
+}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java
index f3b04546..f063fe35 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java
+++ b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java
@@ -301,6 +301,9 @@ private static void deleteExperiment(ExperimentAnnotations expAnnotations, User
// Delete the Panorama Public data catalog entry for this experiment, if one exists
CatalogEntryManager.deleteEntryForExperiment(expAnnotations, user);
+ // Delete the row in DatasetStatus for this experiment, if one exists.
+ DatasetStatusManager.deleteStatusForExperiment(expAnnotations);
+
Table.delete(PanoramaPublicManager.getTableInfoExperimentAnnotations(), expAnnotations.getId());
if(expAnnotations.isJournalCopy() && expAnnotations.getShortUrl() != null)
diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java
index 8895fd8c..d3333f9e 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java
+++ b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java
@@ -49,6 +49,7 @@
import org.labkey.api.security.User;
import org.labkey.api.security.UserManager;
import org.labkey.api.security.UserPrincipal;
+import org.labkey.api.security.permissions.AdminPermission;
import org.labkey.api.security.permissions.Permission;
import org.labkey.api.security.roles.FolderAdminRole;
import org.labkey.api.security.roles.ProjectAdminRole;
@@ -75,8 +76,10 @@
import org.labkey.panoramapublic.PanoramaPublicSchema;
import org.labkey.panoramapublic.model.CatalogEntry;
import org.labkey.panoramapublic.model.DataLicense;
+import org.labkey.panoramapublic.model.DatasetStatus;
import org.labkey.panoramapublic.model.ExperimentAnnotations;
import org.labkey.panoramapublic.model.Journal;
+import org.labkey.panoramapublic.security.PanoramaPublicSubmitterPermission;
import org.labkey.panoramapublic.view.publish.CatalogEntryWebPart;
import org.labkey.panoramapublic.view.publish.ShortUrlDisplayColumnFactory;
@@ -322,6 +325,8 @@ public Class getDisplayValueClass()
catalogEntryCol.setDisplayColumnFactory(CatalogEntryIconColumn::new);
addColumn(catalogEntryCol);
+ addColumn(getDatasetStatusCol(cf));
+
List visibleColumns = new ArrayList<>();
visibleColumns.add(FieldKey.fromParts("Share"));
visibleColumns.add(FieldKey.fromParts("Title"));
@@ -404,6 +409,23 @@ private ExprColumn getCatalogEntryCol()
return col;
}
+ private ExprColumn getDatasetStatusCol(ContainerFilter cf)
+ {
+ SQLFragment datasetStatusSql = new SQLFragment(" (SELECT status.Id AS DatasetStatus ")
+ .append(" FROM ").append(PanoramaPublicManager.getTableInfoDatasetStatus(), "status")
+ .append(" WHERE ")
+ .append(" status.experimentAnnotationsId = ").append(ExprColumn.STR_TABLE_ALIAS).append(".Id")
+ .append(") ");
+ ExprColumn col = new ExprColumn(this, "DatasetStatus", datasetStatusSql, JdbcType.INTEGER);
+ col.setDescription("Dataset Status");
+ col.setDisplayColumnFactory(DatasetStatusColumn::new);
+
+ col.setFk(QueryForeignKey
+ .from(getUserSchema(), cf)
+ .to(PanoramaPublicSchema.TABLE_DATASET_STATUS, "Id", null));
+ return col;
+ }
+
private ExprColumn getIsPublicCol()
{
// Panorama Public dataset folders do not inherit permissions from the parent folder, so we don't need to worry about that case.
@@ -829,7 +851,7 @@ public void renderGridCellContents(RenderContext ctx, HtmlWriter out)
if (experimentId != null)
{
ExperimentAnnotations expAnnot = ExperimentAnnotationsManager.get(experimentId);
- // Display the catalog entry link only if the user has the required permissions (Admin or PanoramaPublicSubmitter) in the the experiment folder.
+ // Display the catalog entry link only if the user has the required permissions (Admin or PanoramaPublicSubmitter) in the experiment folder.
if (expAnnot != null && CatalogEntryWebPart.canBeDisplayed(expAnnot, user))
{
CatalogEntry entry = catalogEntryId == null ? null : CatalogEntryManager.get(catalogEntryId);
@@ -851,4 +873,71 @@ public void renderGridCellContents(RenderContext ctx, HtmlWriter out)
out.write(HtmlString.NBSP);
}
}
+
+ public static class DatasetStatusColumn extends DataColumn
+ {
+ private final FieldKey EXPT_ANNOTATIONS_ID_COL = new FieldKey(getColumnInfo().getFieldKey(), "experimentAnnotationsId");
+
+ public DatasetStatusColumn(ColumnInfo col)
+ {
+ super(col);
+ super.setCaption("Dataset Status");
+ }
+
+ @Override
+ public Object getDisplayValue(RenderContext ctx)
+ {
+ User user = ctx.getViewContext().getUser();
+ if (user == null || user.isGuest())
+ {
+ return "";
+ }
+ Integer statusId = ctx.get(getColumnInfo().getFieldKey(), Integer.class);
+
+ // Get the experiment connected with this status Id.
+ Integer experimentId = ctx.get(EXPT_ANNOTATIONS_ID_COL, Integer.class);
+ if (experimentId != null)
+ {
+ ExperimentAnnotations expAnnot = ExperimentAnnotationsManager.get(experimentId);
+ boolean userHasPermissions = expAnnot != null
+ && expAnnot.getContainer().hasOneOf(user, Set.of(AdminPermission.class, PanoramaPublicSubmitterPermission.class));
+ if (userHasPermissions)
+ {
+ DatasetStatus status = statusId == null ? null : DatasetStatusManager.get(statusId);
+ if (status == null)
+ {
+ return statusId == null ? "" : "NOT FOUND; ID: " + statusId;
+ }
+ String displayStr = status.deletionRequested()
+ ? "Deletion Requested"
+ : status.extensionRequested()
+ ? "Extension Requested"
+ : status.reminderSent()
+ ? "Reminder Sent"
+ : "";
+ return displayStr;
+ }
+ }
+ return "";
+ }
+
+ @Override
+ public void addQueryFieldKeys(Set keys)
+ {
+ super.addQueryFieldKeys(keys);
+ keys.add(EXPT_ANNOTATIONS_ID_COL);
+ }
+
+ @Override
+ public boolean isSortable()
+ {
+ return false;
+ }
+
+ @Override
+ public boolean isFilterable()
+ {
+ return false;
+ }
+ }
}
diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp
index a2b1f056..b4791b4d 100644
--- a/panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp
+++ b/panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp
@@ -28,7 +28,7 @@
function submitForm()
{
const form = document.getElementById("panorama-public-message-form");
- let dataRegion = LABKEY.DataRegions['ExperimentAnnotationsTable'];
+ let dataRegion = LABKEY.DataRegions['ExperimentAnnotations'];
let selectedRowIds = dataRegion.getChecked();
// console.log("Selection count: " + selectedRowIds.length);
let selected = "";
diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp
new file mode 100644
index 00000000..af474fb9
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp
@@ -0,0 +1,154 @@
+<%
+ /*
+ * Copyright (c) 2008-2019 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+%>
+<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %>
+<%@ page import="org.labkey.api.view.HttpView" %>
+<%@ page import="org.labkey.api.view.JspView" %>
+<%@ page import="org.labkey.api.view.template.ClientDependencies" %>
+<%@ page import="org.labkey.panoramapublic.message.PrivateDataReminderSettings" %>
+<%@ page import="org.labkey.api.view.ActionURL" %>
+<%@ page import="org.labkey.panoramapublic.PanoramaPublicController.PrivateDataReminderSettingsForm" %>
+<%@ page import="org.labkey.panoramapublic.PanoramaPublicController.PanoramaPublicAdminViewAction" %>
+<%@ page import="org.labkey.panoramapublic.query.JournalManager" %>
+<%@ page import="org.labkey.panoramapublic.model.Journal" %>
+<%@ page import="java.util.List" %>
+<%@ page import="org.labkey.panoramapublic.PanoramaPublicController.PrivateDataReminderSettingsAction" %>
+<%@ page import="org.labkey.panoramapublic.message.PrivateDataReminderSettings" %>
+<%@ page extends="org.labkey.api.jsp.JspBase" %>
+
+<%!
+ @Override
+ public void addClientDependencies(ClientDependencies dependencies)
+ {
+ dependencies.add("Ext4");
+ }
+%>
+<%
+ JspView view = HttpView.currentView();
+ var form = view.getModelBean();
+ ActionURL panoramaPublicAdminUrl = urlFor(PanoramaPublicAdminViewAction.class);
+
+ List journals = JournalManager.getJournals();
+
+%>
+
+
+
+
+
+
+
+
+
+ Choose Panorama Public Folder :
+
+ <%
+ boolean isFirst = true;
+ for (Journal journal : journals) {
+ %>
+ >
+ <%=h(journal.getName())%>
+
+ <%
+ isFirst = false;
+ }
+ %>
+
+ <%=link("Send Reminders Now").onClick("clickSendRemindersLink();").build()%>
+
+
+
+
\ No newline at end of file
diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp
new file mode 100644
index 00000000..5708b3c5
--- /dev/null
+++ b/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp
@@ -0,0 +1,78 @@
+<%
+ /*
+ * Copyright (c) 2008-2019 LabKey Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+%>
+<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %>
+<%@ page import="org.labkey.api.view.HttpView" %>
+<%@ page import="org.labkey.api.view.JspView" %>
+<%@ page import="org.labkey.panoramapublic.PanoramaPublicController" %>
+<%@ page import="org.labkey.api.view.template.ClientDependencies" %>
+<%@ page extends="org.labkey.api.jsp.JspBase" %>
+
+<%!
+ @Override
+ public void addClientDependencies(ClientDependencies dependencies)
+ {
+ dependencies.add("Ext4");
+ }
+%>
+<%
+ JspView view = HttpView.currentView();
+ var form = view.getModelBean();
+%>
+
+
+
+
+
+
+
+ A reminder message will be sent to the submitters of the selected experiments.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicBaseTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicBaseTest.java
index 8960bcdf..a7da563a 100644
--- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicBaseTest.java
+++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicBaseTest.java
@@ -586,6 +586,22 @@ protected void enableCatalogEntries()
waitForText("Panorama Public catalog entry settings were saved");
}
+ protected void verifyIsPublicColumn(String panoramaPublicProject, String experimentTitle, boolean isPublic)
+ {
+ if (isImpersonating())
+ {
+ stopImpersonating(true);
+ }
+ goToProjectHome(panoramaPublicProject);
+
+ DataRegionTable expListTable = DataRegionTable.findDataRegionWithinWebpart(this, "Targeted MS Experiment List");
+ expListTable.ensureColumnsPresent("Title", "DataVersion", "Public");
+ expListTable.setFilter("Title", "Equals", experimentTitle);
+ expListTable.setFilter("DataVersion", "Equals", "1");
+ assertEquals(1, expListTable.getDataRowCount());
+ assertEquals(isPublic ? "Yes" : "No", expListTable.getDataAsText(0, "Public"));
+ }
+
@Override
protected void doCleanup(boolean afterTest) throws TestTimeoutException
{
diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicMakePublicTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicMakePublicTest.java
index f81de6b8..4abe965b 100644
--- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicMakePublicTest.java
+++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicMakePublicTest.java
@@ -13,7 +13,6 @@
import org.labkey.test.components.panoramapublic.TargetedMsExperimentWebPart;
import org.labkey.test.pages.admin.PermissionsPage;
import org.labkey.test.util.ApiPermissionsHelper;
-import org.labkey.test.util.DataRegionTable;
import org.labkey.test.util.Ext4Helper;
import org.labkey.test.util.PermissionsHelper;
import org.openqa.selenium.NoSuchElementException;
@@ -227,22 +226,6 @@ private void verifyPermissions(String userProject, String userFolder, String pan
}
}
- private void verifyIsPublicColumn(String panoramaPublicProject, String experimentTitle, boolean isPublic)
- {
- if (isImpersonating())
- {
- stopImpersonating(true);
- }
- goToProjectHome(panoramaPublicProject);
-
- DataRegionTable expListTable = DataRegionTable.findDataRegionWithinWebpart(this, "Targeted MS Experiment List");
- expListTable.ensureColumnsPresent("Title", "DataVersion", "Public");
- expListTable.setFilter("Title", "Equals", experimentTitle);
- expListTable.setFilter("DataVersion", "Equals", "1");
- assertEquals(1, expListTable.getDataRowCount());
- assertEquals(isPublic ? "Yes" : "No", expListTable.getDataAsText(0, "Public"));
- }
-
private String getReviewerEmail(String panoramaPublicProject, String panoramaPublicFolder)
{
// Get the reviewer's email from the notification messages
diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java
new file mode 100644
index 00000000..3619b747
--- /dev/null
+++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java
@@ -0,0 +1,453 @@
+package org.labkey.test.tests.panoramapublic;
+
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.labkey.test.BaseWebDriverTest;
+import org.labkey.test.Locator;
+import org.labkey.test.TestTimeoutException;
+import org.labkey.test.categories.External;
+import org.labkey.test.categories.MacCossLabModules;
+import org.labkey.test.pages.LabkeyErrorPage;
+import org.labkey.test.util.ApiPermissionsHelper;
+import org.labkey.test.util.DataRegionTable;
+
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+@Category({External.class, MacCossLabModules.class})
+@BaseWebDriverTest.ClassTimeout(minutes = 7)
+public class PrivateDataReminderTest extends PanoramaPublicBaseTest
+{
+ private static final String SKY_FILE_1 = "MRMer.zip";
+
+ static final String SUBMITTER_1 = "submitter_1@panoramapublic.test";
+ static final String SUBMITTER_2 = "submitter_2@panoramapublic.test";
+ static final String SUBMITTER_3 = "submitter_3@panoramapublic.test";
+ private static final String ADMIN_1 = "admin_1@panoramapublic.test";
+ private static final String ADMIN_2 = "admin_2@panoramapublic.test";
+ private static final String ADMIN_3 = "admin_3@panoramapublic.test";
+
+ private static final String REMINDER_MESSAGE_TITLE = "Title: Action Required: Status Update for Your Private Data on Panorama Public";
+ private static final String EXTENSION_MESSAGE_TITLE = "Title: Private Status Extended - ";
+ private static final String DELETION_MESSAGE_TITLE = "Title: Data Deletion Requested - ";
+ private static final String MESSAGE_PAGE_TITLE = "Submitted - ";
+
+
+ @Test
+ public void testPrivateDataReminder()
+ {
+ String panoramaPublicProject = PANORAMA_PUBLIC;
+ goToProjectHome(panoramaPublicProject);
+ ApiPermissionsHelper permissionsHelper = new ApiPermissionsHelper(this);
+ permissionsHelper.setSiteGroupPermissions("Guests", "Reader");
+
+
+ String testProject = getProjectName();
+
+ DataFolderInfo folderInfo_1 = createAndSubmitFolder(testProject,
+ "Private Data 1",
+ panoramaPublicProject,
+ "Private Data 1 Copy",
+ "Test for private data message reminder - DATA ONE",
+ SUBMITTER_1, "One",
+ ADMIN_1);
+
+ DataFolderInfo folderInfo_2 = createAndSubmitFolder(testProject,
+ "Private Data 2",
+ panoramaPublicProject,
+ "Private Data 2 Copy",
+ "Test for private data message reminder - DATA TWO",
+ SUBMITTER_2, "Two",
+ ADMIN_2);
+
+ DataFolderInfo folderInfo_3 = createAndSubmitFolder(testProject,
+ "Private Data 3",
+ panoramaPublicProject,
+ "Private Data 3 Copy",
+ "Test for private data message reminder - DATA THREE",
+ SUBMITTER_3, "Three",
+ ADMIN_3);
+
+ log("Making data 3 public.");
+ makePublic(panoramaPublicProject, folderInfo_3);
+ folderInfo_3.setPublic(true);
+
+ log("Verifying data 1 is private");
+ verifyIsPublicColumn(panoramaPublicProject, folderInfo_1.getExperimentTitle(), false);
+ log("Verifying data 2 is private");
+ verifyIsPublicColumn(panoramaPublicProject, folderInfo_2.getExperimentTitle(), false);
+ log("Verifying data 3 is public");
+ verifyIsPublicColumn(panoramaPublicProject, folderInfo_3.getExperimentTitle(), true);
+
+ List dataFolderInfos = List.of(folderInfo_1, folderInfo_2, folderInfo_3);
+ testSendingReminders(panoramaPublicProject, dataFolderInfos);
+ }
+
+ private DataFolderInfo createAndSubmitFolder(String testProject, String sourceFolder,
+ String panoramaPublicProject, String targetFolder,
+ String experimentTitle,
+ String submitter, String submitterName, String admin)
+ {
+ log(String.format("Creating folder '%s' and copying to Panorama Public folder '%s'.", sourceFolder, targetFolder));
+ String shortAccessUrl = setupFolderSubmitAndCopy(testProject, sourceFolder, targetFolder, experimentTitle,
+ submitter, submitterName, admin, SKY_FILE_1);
+ goToProjectFolder(panoramaPublicProject, targetFolder);
+ goToExperimentDetailsPage();
+ int exptAnnotationsId = Integer.parseInt(portalHelper.getUrlParam("id"));
+ return new DataFolderInfo(sourceFolder, targetFolder, shortAccessUrl, experimentTitle, exptAnnotationsId, submitter);
+ }
+
+ private void gotoSupportMessage(DataFolderInfo folderInfo)
+ {
+ portalHelper.clickWebpartMenuItem("Targeted MS Experiment", true, "Support Messages");
+ waitForText("Submitted - " + folderInfo.getShortUrl());
+ }
+
+ private void makePublic(String projectName, DataFolderInfo folderInfo)
+ {
+ if (isImpersonating())
+ {
+ stopImpersonating(true);
+ }
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ impersonate(folderInfo.getSubmitter());
+ goToDashboard();
+ makeDataPublic(true);
+ stopImpersonating();
+ }
+
+ private void testSendingReminders(String projectName, List dataFolderInfos)
+ {
+ goToProjectHome(projectName);
+ goToDataPipeline();
+ int pipelineJobCount = getPipelineStatusValues().size();
+
+ List privateData = dataFolderInfos.stream().filter(f -> !f.isPublic()).toList();
+ int privateDataCount = privateData.size();
+ assertEquals(2, privateDataCount);
+
+ log("Changing reminder settings. Setting reminder frequency to 0.");
+ saveSettings("2", "12", "0");
+
+ // Do not select any experiments. Job will not run.
+ log("Attempt to send reminders without selecting any experiments. Job should not run.");
+ postRemindersNoExperimentsSelected(projectName, privateDataCount);
+
+ // Post reminders. None should get posted since the delayUntilFirstReminder is set to 12 months.
+ log("Posting reminders. Select all experiment rows.");
+ postReminders(projectName, false, privateDataCount, -1, ++pipelineJobCount);
+ // Verify that no reminders posted
+ verifyNoReminderPosted(projectName, dataFolderInfos);
+ String message = String.format("Skipping reminder for experiment Id %d - First reminder not due until ", privateData.get(0).getExperimentAnnotationsId());
+ String message2 = String.format("Skipping reminder for experiment Id %d - First reminder not due until ", privateData.get(1).getExperimentAnnotationsId());
+ verifyPipelineJobLogMessage(projectName, message, message2, "Skipped posting reminders for 2 experiments");
+
+ log("Changing reminder settings. Setting delay until first reminder to 0.");
+ saveSettings("2", "0", "0");
+
+ // Post reminders in test mode.
+ log("Posting reminders in test mode. Select all experiment rows.");
+ postRemindersInTestMode(projectName, privateDataCount, -1, ++pipelineJobCount);
+ // Verify that no reminders posted since test mode was checked
+ verifyNoReminderPosted(projectName, dataFolderInfos);
+
+ // Now really post the reminder. Select only the first experiment.
+ postReminders(projectName, false, privateDataCount, 1, ++pipelineJobCount);
+ verifyReminderPosted(projectName, privateData.get(0), 1); // Reminder should be posted only to the selected experiment
+ verifyNoReminderPosted(projectName, privateData.get(1)); // No reminder on the second experiment, since it was not selected
+ verifyNoReminderPosted(projectName, dataFolderInfos.get(2)); // No reminder since this is public data
+
+ // Change the reminder frequency to 1.
+ log("Changing reminder settings. Setting reminder frequency to 1.");
+ saveSettings("2", "0", "1");
+ // Post reminders again. Since reminder frequency is set to 1, no reminders will be posted to the first data.
+ postReminders(projectName, false, privateDataCount, -1, ++pipelineJobCount);
+ verifyReminderPosted(projectName, privateData.get(0), 1); // No new reminders since reminder frequency is set to 1.
+ verifyReminderPosted(projectName, privateData.get(1), 1);
+ message = String.format("Skipping reminder for experiment Id %d - Recent reminder already sent", privateData.get(0).getExperimentAnnotationsId());
+ verifyPipelineJobLogMessage(projectName, message, "Skipped posting reminders for 1 experiment");
+
+ // Change reminder frequency to 0 again.
+ log("Changing reminder settings. Setting reminder frequency to 0.");
+ saveSettings("2", "0", "0");
+
+ // Request extension for the first experiment.
+ log("Requesting extension for experiment Id " + privateData.get(0).getExperimentAnnotationsId());
+ requestExtension(projectName, privateData.get(0));
+ postReminders(projectName, false, privateDataCount, -1, ++pipelineJobCount);
+ verifyReminderPosted(projectName, privateData.get(0),1); // No new reminders since extension requested.
+ verifyReminderPosted(projectName, privateData.get(1), 2); // Reminder posted since reminder frequency is 0.
+ message = String.format("Skipping reminder for experiment Id %d - Submitter requested an extension. Extension is current",
+ privateData.get(0).getExperimentAnnotationsId());
+ verifyPipelineJobLogMessage(projectName, message, "Skipped posting reminders for 1 experiment");
+
+ // Request deletion for the second experiment.
+ log("Requesting deletion for experiment Id " + privateData.get(1).getExperimentAnnotationsId());
+ requestDeletion(projectName, privateData.get(1));
+ // Post reminders again - none should be posted
+ postReminders(projectName, false, privateDataCount, -1, ++pipelineJobCount);
+ verifyReminderPosted(projectName, privateData.get(0),1); // No new reminders since extension requested.
+ verifyReminderPosted(projectName, privateData.get(1), 2); // No new reminders since deletion requested.
+ message2 = String.format("Skipping reminder for experiment Id %d - Submitter has requested deletion", privateData.get(1).getExperimentAnnotationsId());
+ verifyPipelineJobLogMessage(projectName, message, message2, "Skipped posting reminders for 2 experiments");
+ }
+
+ private void requestExtension(String projectName, DataFolderInfo folderInfo)
+ {
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ gotoSupportMessage(folderInfo);
+
+ String actionName = "Request Extension";
+ checkUnauthorizedAccess(actionName);
+
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ gotoSupportMessage(folderInfo);
+ impersonate(folderInfo.getSubmitter());
+
+ assertTextPresent(actionName);
+ click(Locator.linkWithText(actionName));
+ waitForText("Request Extension For Panorama Public Data");
+ assertTextPresent("You are requesting an extension for the private data on Panorama Public at " + folderInfo.getShortUrl());
+ clickButton("OK", 0);
+ waitForText("An extension request was successfully submitted for the data at " + folderInfo.getShortUrl());
+
+ stopImpersonating();
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ gotoSupportMessage(folderInfo);
+ assertTextPresent(EXTENSION_MESSAGE_TITLE + folderInfo.getShortUrl());
+ }
+
+ private void requestDeletion(String projectName, DataFolderInfo folderInfo)
+ {
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ gotoSupportMessage(folderInfo);
+
+ String actionName = "Request Deletion";
+ checkUnauthorizedAccess(actionName);
+
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ gotoSupportMessage(folderInfo);
+ impersonate(folderInfo.getSubmitter());
+
+ assertTextPresent(actionName);
+ click(Locator.linkWithText(actionName));
+ waitForText("Request Deletion For Panorama Public Data");
+ assertTextPresent("You are requesting deletion of the private data on Panorama Public at " + folderInfo.getShortUrl());
+ clickButton("OK", 0);
+ waitForText("A deletion request was successfully submitted for the data at " + folderInfo.getShortUrl());
+
+ stopImpersonating();
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ gotoSupportMessage(folderInfo);
+ assertTextPresent(DELETION_MESSAGE_TITLE + folderInfo.getShortUrl());
+ }
+
+ private void checkUnauthorizedAccess(String actionName)
+ {
+ impersonate(SUBMITTER_3); // This submitter does not have access
+ waitForText(actionName);
+ click(Locator.linkWithText(actionName));
+ new LabkeyErrorPage(getDriver()).assertUnauthorized(checker());
+ stopImpersonating(false); // Don't go home
+ waitForText(actionName);
+ }
+
+ private void verifyPipelineJobLogMessage(String project, String... message)
+ {
+ goToProjectHome(project);
+ goToDataPipeline();
+ goToDataPipeline().clickStatusLink(0);
+ assertTextPresent(message);
+ }
+
+ private void verifyNoReminderPosted(String projectName, List folderInfos)
+ {
+ for (DataFolderInfo folderInfo: folderInfos)
+ {
+ verifyNoReminderPosted(projectName, folderInfo);
+ }
+ }
+
+ private void verifyNoReminderPosted(String projectName, DataFolderInfo folderInfo)
+ {
+ verifyReminderPosted(projectName, folderInfo, 0);
+ }
+
+ private void verifyReminderPosted(String projectName, DataFolderInfo folderInfo, int count)
+ {
+ goToProjectFolder(projectName, folderInfo.getTargetFolder());
+ gotoSupportMessage(folderInfo);
+ waitForText(MESSAGE_PAGE_TITLE + folderInfo.getShortUrl());
+ if (count == 0)
+ {
+ assertTextNotPresent(REMINDER_MESSAGE_TITLE);
+ }
+ else
+ {
+ assertTextPresent(REMINDER_MESSAGE_TITLE, 1);
+ assertTextPresent("Is the paper associated with this work already published?", count);
+ }
+ }
+
+ private void saveSettings(String extensionLength, String delayUntilFirstReminder, String reminderFrequency)
+ {
+ goToAdminConsole().goToSettingsSection();
+ clickAndWait(Locator.linkWithText("Panorama Public"));
+ clickAndWait(Locator.linkWithText("Private Data Reminder Settings"));
+
+ setFormElement(Locator.input("delayUntilFirstReminder"), delayUntilFirstReminder);
+ setFormElement(Locator.input("reminderFrequency"), reminderFrequency);
+ setFormElement(Locator.input("extensionLength"), extensionLength);
+ clickButton("Save", 0);
+ waitForText("Private data reminder settings saved");
+ clickAndWait(Locator.linkWithText("Back to Private Data Reminder Settings"));
+
+ // clickAndWait(Locator.linkWithText("Private Data Reminder Settings"));
+ assertEquals(String.valueOf(delayUntilFirstReminder), getFormElement(Locator.input("delayUntilFirstReminder")));
+ assertEquals(String.valueOf(reminderFrequency), getFormElement(Locator.input("reminderFrequency")));
+ assertEquals(String.valueOf(extensionLength), getFormElement(Locator.input("extensionLength")));
+ }
+
+ private void postRemindersNoExperimentsSelected(String projectName, int expectedExperimentCount)
+ {
+ postReminders(projectName, true, expectedExperimentCount, 0, 0);
+ }
+
+ private void postRemindersInTestMode(String projectName, int expectedExperimentCount, int selectExperimentCount, int pipelineJobCount)
+ {
+ postReminders(projectName, true, expectedExperimentCount, selectExperimentCount, pipelineJobCount);
+ }
+
+
+ private void postReminders(String projectName, boolean testMode, int expectedExperimentCount, int selectRowCount, int pipelineJobCount)
+ {
+ goToSendRemindersPage(projectName);
+
+ DataRegionTable table = new DataRegionTable("ExperimentAnnotations", getDriver());
+ assertEquals(expectedExperimentCount, table.getDataRowCount());
+
+ table.clearAllFilters();
+ table.uncheckAllOnPage();
+
+ if (selectRowCount == -1)
+ {
+ table.checkAllOnPage();
+ }
+ else
+ {
+ for(int i = 0; i < selectRowCount; i++)
+ {
+ table.checkCheckbox(i);
+ }
+ }
+
+ if (testMode)
+ {
+ checkCheckbox(Locator.checkboxByName("testMode"));
+ }
+
+ clickButton("Post Reminders", 0);
+
+ if (selectRowCount == 0)
+ {
+ waitForText("Please select at least one experiment");
+ if (testMode)
+ {
+ assertChecked(Locator.checkboxByName("testMode"));
+ }
+ }
+ else
+ {
+ waitForPipelineJobsToComplete(pipelineJobCount, "Post private data reminder messages", false);
+ goToDataPipeline().clickStatusLink(0);
+ if (testMode)
+ {
+ assertTextPresent("RUNNING IN TEST MODE - MESSAGES WILL NOT BE POSTED.");
+ }
+ else
+ {
+ assertTextNotPresent("RUNNING IN TEST MODE - MESSAGES WILL NOT BE POSTED.");
+ }
+ }
+ }
+
+ private void goToSendRemindersPage(String projectName)
+ {
+ goToAdminConsole().goToSettingsSection();
+ clickAndWait(Locator.linkWithText("Panorama Public"));
+ clickAndWait(Locator.linkWithText("Private Data Reminder Settings"));
+ selectOptionByText(Locator.name("journal"), projectName);
+ clickAndWait(Locator.linkWithText("Send Reminders Now"));
+ waitForText(projectName, "A reminder message will be sent to the submitters of the selected experiments");
+ }
+
+ @Override
+ protected void doCleanup(boolean afterTest) throws TestTimeoutException
+ {
+ _userHelper.deleteUsers(false,SUBMITTER_1, SUBMITTER_2, SUBMITTER_3, ADMIN_1, ADMIN_2, ADMIN_3);
+
+ super.doCleanup(afterTest);
+ }
+
+ private static class DataFolderInfo
+ {
+ private final String _sourceFolder;
+ private final String _targetFolder;
+ private final String _shortUrl;
+ private final String _experimentTitle;
+ private final int _experimentAnnotationsId;
+ private final String _submitter;
+ private boolean _isPublic = false;
+
+ public DataFolderInfo(String sourceFolder, String targetFolder, String shortUrl, String experimentTitle, int experimentAnnotationsId, String submitter)
+ {
+ _sourceFolder = sourceFolder;
+ _targetFolder = targetFolder;
+ _shortUrl = shortUrl;
+ _experimentTitle = experimentTitle;
+ _experimentAnnotationsId = experimentAnnotationsId;
+ _submitter = submitter;
+ }
+
+ public String getSourceFolder()
+ {
+ return _sourceFolder;
+ }
+
+ public String getTargetFolder()
+ {
+ return _targetFolder;
+ }
+
+ public String getShortUrl()
+ {
+ return _shortUrl;
+ }
+
+ public String getExperimentTitle()
+ {
+ return _experimentTitle;
+ }
+
+ public int getExperimentAnnotationsId()
+ {
+ return _experimentAnnotationsId;
+ }
+
+ public String getSubmitter()
+ {
+ return _submitter;
+ }
+
+ public boolean isPublic()
+ {
+ return _isPublic;
+ }
+
+ public void setPublic(boolean isPublic)
+ {
+ _isPublic = isPublic;
+ }
+ }
+}