From bb24674ac337705ddd64336b320658aeed084500 Mon Sep 17 00:00:00 2001 From: vagisha Date: Wed, 21 May 2025 15:08:05 -0700 Subject: [PATCH 01/18] In proress - send message to submitters of data still private on Panorama Public --- .../panoramapublic-25.001-25.002.sql | 21 ++ .../PanoramaPublicController.java | 237 +++++++++++++++++- .../panoramapublic/PanoramaPublicManager.java | 5 + .../panoramapublic/PanoramaPublicModule.java | 7 + .../PanoramaPublicNotification.java | 109 ++++++++ .../panoramapublic/PanoramaPublicSchema.java | 8 + .../message/PrivateDataMessageScheduler.java | 119 +++++++++ .../panoramapublic/model/DatasetStatus.java | 110 ++++++++ .../pipeline/PrivateDataReminderJob.java | 210 ++++++++++++++++ .../query/DatasetStatusManager.java | 40 +++ .../query/DatasetStatusTableInfo.java | 49 ++++ 11 files changed, 914 insertions(+), 1 deletion(-) create mode 100644 panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql create mode 100644 panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java create mode 100644 panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java create mode 100644 panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java create mode 100644 panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java create mode 100644 panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java diff --git a/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql b/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql new file mode 100644 index 00000000..d49bbd02 --- /dev/null +++ b/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql @@ -0,0 +1,21 @@ +CREATE TABLE panoramapublic.DatasetStatus +( + _ts TIMESTAMP, + Id SERIAL NOT NULL, + CreatedBy USERID, + Created TIMESTAMP, + ModifiedBy USERID, + Modified TIMESTAMP, + + ShortUrl ENTITYID NOT NULL, + ReminderDate DATETIME, + ExtensionRequestedDate DATETIME, + DeletionRequestedDate DATETIME, + + CONSTRAINT PK_DatasetStatus PRIMARY KEY (Id), + + CONSTRAINT FK_DatasetStatus_ShortUrl FOREIGN KEY (ShortUrl) REFERENCES core.shorturl (entityId), + + CONSTRAINT UQ_DatasetStatus_ShortUrl UNIQUE (ShortUrl) +); +CREATE INDEX IX_DatasetStatus_ShortUrl ON panoramapublic.CatalogEntry(ShortUrl); diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index d50fe52d..ab7e02c9 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -157,6 +157,7 @@ import org.labkey.panoramapublic.datacite.DoiMetadata; 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.model.JournalExperiment; @@ -191,6 +192,7 @@ import org.labkey.panoramapublic.query.CatalogEntryManager.CatalogEntryType; import org.labkey.panoramapublic.query.DataValidationManager; import org.labkey.panoramapublic.query.DataValidationManager.MissingMetadata; +import org.labkey.panoramapublic.query.DatasetStatusManager; import org.labkey.panoramapublic.query.ExperimentAnnotationsManager; import org.labkey.panoramapublic.query.JournalManager; import org.labkey.panoramapublic.query.ModificationInfoManager; @@ -228,6 +230,7 @@ import java.util.Arrays; import java.util.Base64; import java.util.Collections; +import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -10021,9 +10024,241 @@ public static ActionURL getViewExperimentModificationsURL(int experimentAnnotati result.addParameter("id", experimentAnnotationsId); return result; } + + @RequiresLogin + public class RequestExtensionAction extends ConfirmAction + { + + private ExperimentAnnotations _exptAnnotations; + private DatasetStatus _datasetStatus; + + @Override + public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception + { + setTitle("Request Extension For Panorama Public Data"); + HtmlView view = new HtmlView(DIV( + DIV("You are requesting an extension for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), + DIV("Title: " + _exptAnnotations.getTitle()), + DIV("Submitted on: " + _exptAnnotations.getCreated()), + DIV("Submitter: " + _exptAnnotations.getSubmitterName()) + )); + view.setTitle("Request Extension"); + return view; + } + + @Override + public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) + { + _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, getUser(), errors); + if (_exptAnnotations == null) + { + return; + } + + ShortURLRecord shortUrl = _exptAnnotations.getShortUrl(); + _datasetStatus = DatasetStatusManager.getForShortUrl(shortUrl); + if (_datasetStatus != null) + { + if (_datasetStatus.isExtensionValid()) + { + errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + shortUrl.renderShortURL() + + ". The extension is valid until " + _datasetStatus.extensionValidUntil()); + } + else if (_datasetStatus.deletionRequested()) + { + errors.reject(ERROR_MSG, "A deletion request was submitted on " + _datasetStatus.getDeletionRequestedDate() + " for the data with short URL " + shortUrl.renderShortURL()); + } + } + } + + @Override + public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throws Exception + { + DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(_exptAnnotations.getShortUrl()); + try(DbScope.Transaction transaction = CoreSchema.getInstance().getSchema().getScope().ensureTransaction()) + { + if (datasetStatus == null) // TODO: Can this ever be null? + { + datasetStatus = new DatasetStatus(); + datasetStatus.setShortUrl(_exptAnnotations.getShortUrl()); + datasetStatus.setExtensionRequestedDate(new Date()); + DatasetStatusManager.save(datasetStatus, getUser()); + } + else + { + datasetStatus.setExtensionRequestedDate(new Date()); + DatasetStatusManager.update(datasetStatus, getUser()); + } + + // 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()); + + transaction.commit(); + } + + return true; + } + + @Override + public ModelAndView getSuccessView(ShortUrlForm shortUrlForm) + { + return new HtmlView(DIV("An extension request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(), + DIV("Extension is valid until " + _datasetStatus.extensionValidUntil()))); + } + + @Override + public @NotNull URLHelper getSuccessURL(ShortUrlForm shortUrlForm) + { + return null; + } + } + + @RequiresLogin + public class RequestDeletionAction extends ConfirmAction + { + + private ExperimentAnnotations _exptAnnotations; + private DatasetStatus _datasetStatus; + + @Override + public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception + { + setTitle("Request Deletion For Panorama Public Data"); + HtmlView view = new HtmlView(DIV( + DIV("You are requesting deletion for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), + DIV("Title: " + _exptAnnotations.getTitle()), + DIV("Submitted on: " + _exptAnnotations.getCreated()), + DIV("Submitter: " + _exptAnnotations.getSubmitterName()) + )); + view.setTitle("Request Deletion"); + return view; + } + + @Override + public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) + { + _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, getUser(), errors); + if (_exptAnnotations == null) + { + return; + } + + ShortURLRecord shortUrl = _exptAnnotations.getShortUrl(); + _datasetStatus = DatasetStatusManager.getForShortUrl(shortUrl); + if (_datasetStatus != null) + { + if (_datasetStatus.deletionRequested()) + { + errors.reject(ERROR_MSG, "A deletion request was already submitted on " + _datasetStatus.getDeletionRequestedDate() + " for the data with short URL " + shortUrl.renderShortURL()); + } + } + } + + @Override + public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throws Exception + { + DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(_exptAnnotations.getShortUrl()); + try(DbScope.Transaction transaction = CoreSchema.getInstance().getSchema().getScope().ensureTransaction()) + { + if (datasetStatus == null) // TODO: Can this ever be null? + { + datasetStatus = new DatasetStatus(); + datasetStatus.setShortUrl(_exptAnnotations.getShortUrl()); + datasetStatus.setDeletionRequestedDate(new Date()); + DatasetStatusManager.save(datasetStatus, getUser()); + } + else + { + datasetStatus.setDeletionRequestedDate(new Date()); + DatasetStatusManager.update(datasetStatus, getUser()); + } + + // 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()); + + transaction.commit(); + } + + return true; + } + + @Override + public ModelAndView getSuccessView(ShortUrlForm shortUrlForm) + { + return new HtmlView(DIV("An extension request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(), + DIV("Extension is valid until " + _datasetStatus.extensionValidUntil()))); + } + + @Override + public @NotNull URLHelper getSuccessURL(ShortUrlForm shortUrlForm) + { + return null; + } + } + + 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, User user, 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; + } + + // User requesting the extension / deletion must be the data submitter or lab head + if (!(user.equals(exptAnnotations.getSubmitterUser()) && user.equals(exptAnnotations.getLabHeadUser()))) + { + errors.reject(ERROR_MSG, "Status change can be requested only by the data submitter or lab head."); + 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; + } + // ------------------------------------------------------------------------ - // END Actions to create, delete, edit and view experiment annotations. + // END Actions to request extension or deletion for a private dataset. // ------------------------------------------------------------------------ + + public static ActionURL getCopyExperimentURL(int experimentAnnotationsId, int journalId, Container container) { ActionURL result = new ActionURL(PanoramaPublicController.CopyExperimentAction.class, container); 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 0a5c28cb..83e4c495 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.PrivateDataMessageScheduler; import org.labkey.panoramapublic.model.Journal; import org.labkey.panoramapublic.model.speclib.SpecLibKey; import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineProvider; @@ -356,6 +357,12 @@ public Set getSchemaNames() return Collections.singleton(PanoramaPublicSchema.SCHEMA_NAME); } + @Override + public void startBackgroundThreads() + { + PrivateDataMessageScheduler.getInstance().initializeTimer(); + } + @NotNull @Override public Set getIntegrationTests() diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java index 57bba24b..12c1cdd7 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -20,11 +20,14 @@ import org.labkey.api.view.NotFoundException; import org.labkey.panoramapublic.datacite.DataCiteException; import org.labkey.panoramapublic.datacite.DataCiteService; +import org.labkey.panoramapublic.model.DatasetStatus; import org.labkey.panoramapublic.model.ExperimentAnnotations; import org.labkey.panoramapublic.model.Journal; import org.labkey.panoramapublic.model.JournalExperiment; import org.labkey.panoramapublic.model.Submission; +import org.labkey.panoramapublic.model.validation.Status; 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 +295,110 @@ 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())); + } + /* + Thank you for your request to extend the private status of your data on Panorama Public at . + Your data has been granted an extension for an additional 6 months. You’ll receive another reminder at that time, or you may make the dataset public earlier. + Please feel free to contact us if you have any questions + */ + String messageTitle = "Private Status Extension" +" - " + 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 an extension for an additional " + DatasetStatus.EXTENSION_VALID_MONTHS + " months. ") + .append("You will receive another reminder at that time, or you may make the data public earlier ") + .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 already 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 JournalExperiment je, @NotNull ExperimentAnnotations expAnnotations, + @NotNull User submitter, @NotNull User messagePoster, List notifyUsers) + { + ExperimentAnnotations sourceExperiment = ExperimentAnnotationsManager.get(expAnnotations.getSourceExperimentId()); + String message = getDataStatusReminderMessage(expAnnotations, sourceExperiment); + String title = "Action Required: Status Update for Your Private Dataset on Panorama Public"; + postNotificationFullTitle(journal, je, message, messagePoster, title, StatusOption.Closed, notifyUsers); + } + + public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations exptAnnotations, ExperimentAnnotations sourceExperiment) + { + /* + We hope you are doing well. We’re reaching out regarding your dataset on Panorama Public (https://panoramaweb.org/polyjuice.url), which has been private since January 1, 2024. + Is the paper associated with this work already published? + If yes: Please make your data public by clicking the "Make Public" button in your folder or by clicking [Make Data Public] here. This helps ensure that your valuable research is easily accessible to the community. + If not: You have a couple of options: + Request an Extension - If your paper is still under review, or you need additional time to publish, please let us know by clicking [Request Extension] + Delete from Panorama Public - If you no longer wish to host your data on Panorama Public, please click [Request Deletion]. We will remove your dataset from Panorama Public. However, your source folder (/Hogwarts/Gryffindor/magic-potion) will remain intact, allowing you to resubmit your data in the future if you wish. + If you have any questions or need further assistance, please do not hesitate to respond to this message by clicking here. + Thank you for sharing your research on Panorama Public. We appreciate your commitment to open science and supporting the research community. + */ + + String shortUrl = exptAnnotations.getShortUrl().renderShortURL(); + String makePublicLink = PanoramaPublicController.getMakePublicUrl(exptAnnotations.getId(), exptAnnotations.getContainer()).getURIString(); + StringBuilder message = new StringBuilder(); + message.append("We hope you are doing well. ") + .append("We’re reaching out regarding your dataset on Panorama Public (").append(shortUrl).append("), which has been private since PLACEHOLDER_DATA_SUBMISSION_DATE.") + .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 [**Make Data Public**](").append(makePublicLink).append(")") + .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 to publish, please let us know by clicking [**Request Extension**]().") + .append("\n - **Delete from Panorama Public** - If you no longer wish to host your data on Panorama Public, please click [**Request Deletion**](). ") + .append("We will remove your dataset from Panorama Public. ") + .append("However, your source folder ([/Hogwarts/Gryffindor/magic-potion](http://localhost:8080/labkey/Hogwarts/Gryffindor/magic-potion/project-begin.view)) will remain intact, ") + .append("allowing you to resubmit your data in the future if you wish.") + .append("\n\nIf you have any questions or need further assistance, please do not hesitate to respond to this message by [**clicking here**](__PH__RESPOND__TO__MESSAGE__URL__).") + .append("\n\nThank you for sharing your research on Panorama Public. We appreciate your commitment to open science and supporting the research community."); + 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): /* @@ -314,6 +421,8 @@ this data (available at __PH__DATA__SHORT__URL__), we ask that you do so as soon public static String PLACEHOLDER_RESPOND_TO_MESSAGE_URL = PLACEHOLDER + "RESPOND__TO__MESSAGE__URL__"; public static String PLACEHOLDER_MAKE_DATA_PUBLIC_URL = PLACEHOLDER + "MAKE__DATA__PUBLIC__URL__"; public static String PLACEHOLDER_SHORT_URL = PLACEHOLDER + "DATA__SHORT__URL__"; + + private static String PLACEHOLDER_DATA_SUBMISSION_DATE = PLACEHOLDER + "DATA__SUBMISSION__DATE__"; public static String replaceLinkPlaceholders(@NotNull String text, @NotNull ExperimentAnnotations expAnnotations, @NotNull Announcement announcement, @NotNull Container announcementContainer) { diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicSchema.java index d49ce909..4aad0352 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..70da1392 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java @@ -0,0 +1,119 @@ +package org.labkey.panoramapublic.message; + +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.labkey.api.data.Container; +import org.labkey.api.data.ContainerManager; +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.pipeline.PrivateDataReminderJob; +import org.quartz.DateBuilder; +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.SimpleScheduleBuilder; +import org.quartz.Trigger; +import org.quartz.TriggerBuilder; +import org.quartz.impl.StdSchedulerFactory; + +public class PrivateDataMessageScheduler +{ + private static final Logger _log = LogHelper.getLogger(PrivateDataMessageScheduler.class, "Panorama Public private data reminder message scheduler"); + + private static final PrivateDataMessageScheduler _instance = new PrivateDataMessageScheduler(); + + public static PrivateDataMessageScheduler getInstance() + { + return _instance; + } + + private PrivateDataMessageScheduler(){} + + public void initializeTimer() + { + try + { + Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); + + // Get configured quartz Trigger + Trigger trigger = getTrigger(); + + // Create a quartz job that invokes a pipeline job that posts private data reminder messages + JobDetail job = JobBuilder.newJob(PrivateDataMessageSchedulerJob.class) + .withIdentity(PrivateDataMessageSchedulerJob.class.getCanonicalName()) + .build(); + + // TODO: Add this PrivateDataMessageScheduler instance to the Job context so the Job knows which digest to send + // job.getJobDataMap().put(MESSAGE_SCHEDULER_KEY, this); + + // Schedule trigger to execute the message digest job on the configured schedule + scheduler.scheduleJob(job, trigger); + } + catch (SchedulerException e) + { + throw new RuntimeException("Failed to schedule PrivateDataMessageScheduler job", e); + } + } + + protected Trigger getTrigger() + { + // 1st of every month at 8:00AM +// return TriggerBuilder.newTrigger() +// .withSchedule(CronScheduleBuilder.monthlyOnDayAndHourAndMinute(1, 8, 0)) +// .build(); + return TriggerBuilder.newTrigger() + .withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(1)) + .startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND)) + .build(); + } + + public static class PrivateDataMessageSchedulerJob implements Job + { + private final @Nullable User _user; + + @SuppressWarnings("unused") + public PrivateDataMessageSchedulerJob() + { + this(null); + } + + public PrivateDataMessageSchedulerJob(@Nullable User user) + { + _user = user; + } + + @Override + public void execute(JobExecutionContext context) + { + try + { + Container c = ContainerManager.getRoot(); + ViewBackgroundInfo vbi = new ViewBackgroundInfo(c, _user, null); + PipeRoot root = PipelineService.get().findPipelineRoot(c); + + if (root == null || !root.isValid()) + { + throw new ConfigurationException("No valid pipeline root found in the root container"); + } + + + PipelineJob job = new PrivateDataReminderJob(vbi, PipelineService.get().getPipelineRootSetting(ContainerManager.getRoot()), false); + PipelineService.get().queueJob(job); + } + catch(Exception e) + { + _log.error("Error queuing PrivateDataReminderJob", e); // TODO: Anything else? + // ExceptionUtil.logExceptionToMothership(null, e); + + } + } + } +} 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..21b9d853 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -0,0 +1,110 @@ +package org.labkey.panoramapublic.model; + +import org.jetbrains.annotations.Nullable; +import org.labkey.api.view.ShortURLRecord; + +import java.time.ZoneId; +import java.util.Date; +import java.time.LocalDate; + +public class DatasetStatus extends DbEntity +{ + private ShortURLRecord _shortUrl; + private Date _reminderDate; + private Date _extensionRequestedDate; + private Date _deletionRequestedDate; + + public static final int EXTENSION_VALID_MONTHS = 6; // 6 months + + public ShortURLRecord getShortUrl() + { + return _shortUrl; + } + + public void setShortUrl(ShortURLRecord shortUrl) + { + _shortUrl = shortUrl; + } + + public Date getReminderDate() + { + return _reminderDate; + } + + public void setReminderDate(Date reminderDate) + { + _reminderDate = reminderDate; + } + + public Date getExtensionRequestedDate() + { + return _extensionRequestedDate; + } + + public void setExtensionRequestedDate(Date extensionRequestedDate) + { + _extensionRequestedDate = extensionRequestedDate; + } + + public Date getDeletionRequestedDate() + { + return _deletionRequestedDate; + } + + public void setDeletionRequestedDate(Date deletionRequestedDate) + { + _deletionRequestedDate = deletionRequestedDate; + } + + public boolean deletionRequested() + { + return _deletionRequestedDate != null; + } + + public boolean isExtensionValid() + { + if (_extensionRequestedDate == null) + { + return false; + } + + LocalDate extensionDate = _extensionRequestedDate.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + + LocalDate extensionValidStartDate = LocalDate.now().minusMonths(EXTENSION_VALID_MONTHS); + + return extensionDate.isAfter(extensionValidStartDate); + } + + public boolean isLastReminderRecent() + { + if (_reminderDate == null) + { + return false; + } + + LocalDate reminderDate = _reminderDate.toInstant() + .atZone(ZoneId.systemDefault()) + .toLocalDate(); + + LocalDate extensionValidStartDate = LocalDate.now().minusMonths(EXTENSION_VALID_MONTHS); + + return reminderDate.isAfter(extensionValidStartDate); + } + + public @Nullable Date extensionValidUntil() + { + if (_extensionRequestedDate == null) + { + return null; + } + + return Date.from( + _extensionRequestedDate.toInstant() + .atZone(ZoneId.systemDefault()) + .plusMonths(EXTENSION_VALID_MONTHS) + .toInstant() + ); + } +} 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..e095f09e --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -0,0 +1,210 @@ +package org.labkey.panoramapublic.pipeline; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +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.security.User; +import org.labkey.api.util.FileUtil; +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.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.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class PrivateDataReminderJob extends PipelineJob +{ + private boolean _test; + + protected PrivateDataReminderJob() + { + } + + public PrivateDataReminderJob(ViewBackgroundInfo info, @NotNull PipeRoot root, boolean test) + { + super("Panorama Public", info, root); + setLogFile(root.getRootNioPath().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-private-data-reminder", "log"))); + _test = test; + } + + @Override + public void run() + { + setStatus(TaskStatus.running); + + Journal panoramaPublic = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); + if (panoramaPublic == null) + { + getLogger().error("Panorama Public project does not exist"); + return; + } + + List privateDatasetIds = getPrivateDatasets(panoramaPublic.getProject()); + + postMessage(privateDatasetIds, panoramaPublic); + + setStatus(TaskStatus.complete); + } + + private List getPrivateDatasets(Container projectFolder) + { + Set subFolders = ContainerManager.getAllChildren(projectFolder); + List privateDataIds = new ArrayList<>(); + for (Container folder: subFolders) + { + ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder); + + if (shouldPostReminder(exptAnnotations)) + { + privateDataIds.add(exptAnnotations.getId()); + } + } + + return privateDataIds; + } + + private boolean shouldPostReminder(ExperimentAnnotations exptAnnotations) + { + if (exptAnnotations == null) return false; + if (exptAnnotations.isPublic()) return false; + + DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(exptAnnotations.getShortUrl()); + if (datasetStatus == null) return true; + + // Return false if the submitter has requested deletion + if (datasetStatus.deletionRequested()) return false; + + // Return false if the submitter has requested an extension, and the extension is still valid + if (datasetStatus.isExtensionValid()) return false; + + // Returns false if the last reminder was sent less than a month ago + if (datasetStatus.isLastReminderRecent()) return false; + + return true; + } + + private void postMessage(List expAnnotationIds, Journal panoramaPublic) + { + if (expAnnotationIds.size() == 0) + { + getLogger().info("No private datasets were found."); + return; + } + getLogger().info(String.format("%sPosting reminder message to: %d message threads", _test ? "TEST MODE: " : "", expAnnotationIds.size())); + + int done = 0; + + AnnouncementService announcementSvc = AnnouncementService.get(); + + List experimentNotFound = new ArrayList<>(); + List submissionNotFound = new ArrayList<>(); + List announcementNotFound = new ArrayList<>(); + List submitterNotFound = new ArrayList<>(); + + Container announcementsFolder = panoramaPublic.getSupportContainer(); + + Set exptIds = new HashSet<>(expAnnotationIds); + + try (DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction()) + { + for (Integer experimentAnnotationsId : exptIds) + { + ExperimentAnnotations expAnnotations = ExperimentAnnotationsManager.get(experimentAnnotationsId); + if (expAnnotations == null) + { + getLogger().error("Could not find an experiment with Id: " + experimentAnnotationsId); + experimentNotFound.add(experimentAnnotationsId); + continue; + } + JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(expAnnotations); + if (submission == null || submission.getLatestSubmission() == null) + { + getLogger().error("Could not find a submission request for experiment Id: " + experimentAnnotationsId); + submissionNotFound.add(experimentAnnotationsId); + continue; + } + + Announcement announcement = announcementSvc.getAnnouncement(announcementsFolder, getUser(), submission.getAnnouncementId()); + if (announcement == null) + { + getLogger().error("Could not find the message thread for experiment Id: " + experimentAnnotationsId + + "; announcement Id: " + submission.getAnnouncementId() + " in the folder " + announcementsFolder.getPath()); + announcementNotFound.add(experimentAnnotationsId); + continue; + } + + User submitter = expAnnotations.getSubmitterUser(); + if (submitter == null) + { + getLogger().error("Could not find a submitter user for experiment Id: " + experimentAnnotationsId); + submitterNotFound.add(experimentAnnotationsId); + continue; + } + + if (!_test) + { + // 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(panoramaPublic, submission.getJournalExperiment(), expAnnotations, submitter, getUser(), notifyList); + } + + done++; + getLogger().info(String.format("%s to message thread for experiment Id %d, announcement Id %d. Done: %d", + _test ? "Would post" : "Posted", experimentAnnotationsId, announcement.getRowId(), done)); + + } + transaction.commit(); + } + + if (!experimentNotFound.isEmpty()) + { + getLogger().error("Experiments with the following Ids could not be found: " + StringUtils.join(experimentNotFound, ", ")); + } + if (!submissionNotFound.isEmpty()) + { + getLogger().error("Submission requests were not found for the following experiment Ids: " + StringUtils.join(submissionNotFound, ", ")); + } + if (!announcementNotFound.isEmpty()) + { + getLogger().error("Support message threads were not found for the following experiment Ids: " + StringUtils.join(announcementNotFound, ", ")); + } + if (!submitterNotFound.isEmpty()) + { + getLogger().error("Submitter user was not found for the following experiment Ids: " + StringUtils.join(submissionNotFound, ", ")); + } + } + + @Override + public URLHelper getStatusHref() + { + return null; + } + + @Override + public String getDescription() + { + return "Posts a message to announcement threads of the private datasets"; + } +} 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..8c15091e --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java @@ -0,0 +1,40 @@ +package org.labkey.panoramapublic.query; + +import org.jetbrains.annotations.Nullable; +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.api.view.ShortURLRecord; +import org.labkey.panoramapublic.PanoramaPublicManager; +import org.labkey.panoramapublic.model.DatasetStatus; + +public class DatasetStatusManager +{ + public static DatasetStatus get(int datasetStatusId) + { + return new TableSelector(PanoramaPublicManager.getTableInfoDatasetStatus(),null, null).getObject(datasetStatusId, DatasetStatus.class); + } + + public static @Nullable DatasetStatus getForShortUrl(ShortURLRecord shortUrl) + { + if (shortUrl != null) + { + SimpleFilter filter = new SimpleFilter(); + filter.addCondition(FieldKey.fromParts("ShortUrl"), shortUrl.getEntityId()); + 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()); + } +} 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..a8fa8701 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java @@ -0,0 +1,49 @@ +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("ShortUrl", PanoramaPublicManager.getTableInfoExperimentAnnotations(), "ShortUrl")); + + + var accessUrlCol = wrapColumn("ShortUrl", getRealTable().getColumn("ShortUrl")); + accessUrlCol.setDisplayColumnFactory(new ShortUrlDisplayColumnFactory()); + addColumn(accessUrlCol); + + SQLFragment expColSql = new SQLFragment(" (SELECT Id FROM ") + .append(PanoramaPublicManager.getTableInfoExperimentAnnotations(), "exp") + .append(" WHERE exp.shortUrl = ").append(ExprColumn.STR_TABLE_ALIAS).append(".shortUrl") + .append(") "); + var experimentTitleCol = new ExprColumn(this, "Title", expColSql, JdbcType.VARCHAR); + experimentTitleCol.setFk(QueryForeignKey.from(getUserSchema(), cf).schema(getUserSchema()).to(PanoramaPublicSchema.TABLE_EXPERIMENT_ANNOTATIONS, "Id", null)); + addColumn(experimentTitleCol); + + 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("ShortUrl")); + visibleColumns.add(FieldKey.fromParts("Title")); + visibleColumns.add(FieldKey.fromParts("ReminderDate")); + visibleColumns.add(FieldKey.fromParts("ExtensionRequestedDate")); + visibleColumns.add(FieldKey.fromParts("DeletionRequestedDate")); + setDefaultVisibleColumns(visibleColumns); + } +} From 18e95c51f90a2832d8e5c0c683df467ef727ddb7 Mon Sep 17 00:00:00 2001 From: vagisha Date: Tue, 5 Aug 2025 09:30:08 -0700 Subject: [PATCH 02/18] Added Admin link for private data message settings. Automatic reminder job can be turned on via the admin console --- .../PanoramaPublicController.java | 186 ++++++++++++++---- .../PanoramaPublicNotification.java | 86 +++++--- .../message/PrivateDataMessageScheduler.java | 50 ++--- .../message/PrivateDataMessageSettings.java | 83 ++++++++ .../panoramapublic/model/DatasetStatus.java | 29 ++- .../pipeline/PrivateDataReminderJob.java | 59 ++++-- 6 files changed, 387 insertions(+), 106 deletions(-) create mode 100644 panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 060b55ff..37c30f32 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -78,6 +78,7 @@ import org.labkey.api.module.Module; import org.labkey.api.module.ModuleLoader; import org.labkey.api.module.ModuleProperty; +import org.labkey.api.module.SimpleAction; import org.labkey.api.pipeline.LocalDirectory; import org.labkey.api.pipeline.PipeRoot; import org.labkey.api.pipeline.PipelineJob; @@ -114,7 +115,6 @@ import org.labkey.api.security.permissions.DeletePermission; import org.labkey.api.security.permissions.InsertPermission; import org.labkey.api.security.permissions.ReadPermission; -import org.labkey.api.security.permissions.SiteAdminPermission; import org.labkey.api.security.permissions.UpdatePermission; import org.labkey.api.security.roles.ProjectAdminRole; import org.labkey.api.security.roles.ReaderRole; @@ -125,6 +125,7 @@ import org.labkey.api.targetedms.TargetedMSUrls; import org.labkey.api.util.ButtonBuilder; import org.labkey.api.util.DOM; +import org.labkey.api.util.DateUtil; import org.labkey.api.util.ExceptionUtil; import org.labkey.api.util.FileUtil; import org.labkey.api.util.HtmlString; @@ -153,6 +154,8 @@ import org.labkey.panoramapublic.datacite.DataCiteService; import org.labkey.panoramapublic.datacite.Doi; import org.labkey.panoramapublic.datacite.DoiMetadata; +import org.labkey.panoramapublic.message.PrivateDataMessageScheduler; +import org.labkey.panoramapublic.message.PrivateDataMessageSettings; import org.labkey.panoramapublic.model.CatalogEntry; import org.labkey.panoramapublic.model.DataLicense; import org.labkey.panoramapublic.model.DatasetStatus; @@ -171,6 +174,7 @@ import org.labkey.panoramapublic.model.validation.Status; import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineJob; import org.labkey.panoramapublic.pipeline.PostPanoramaPublicMessageJob; +import org.labkey.panoramapublic.pipeline.PrivateDataReminderJob; import org.labkey.panoramapublic.pipeline.PxDataValidationPipelineJob; import org.labkey.panoramapublic.pipeline.PxValidationPipelineProvider; import org.labkey.panoramapublic.proteomexchange.ChemElement; @@ -318,6 +322,7 @@ public ModelAndView getView(Object o, BindException errors) view.addView(getDataCiteCredentialsLink()); view.addView(getBlueskySettingsLink()); view.addView(getPanoramaPublicCatalogSettingsLink()); + view.addView(getPrivateDataReminderSettingsLink()); view.addView(getPostSupportMessageLink()); view.setFrame(WebPartView.FrameType.PORTAL); view.setTitle("Panorama Public Settings"); @@ -374,6 +379,15 @@ private ModelAndView getPostSupportMessageLink() return null; } + private ModelAndView getPrivateDataReminderSettingsLink() + { + ActionURL url = new ActionURL(PrivateDataReminderSettingsAction.class, getContainer()); + return new HtmlView(DIV( + at(style, "margin-top:20px;"), + LinkBuilder.labkeyLink("Private Data Reminder Settings", url) + )); + } + @Override public void addNavTrail(NavTree root) { @@ -10076,7 +10090,6 @@ public static ActionURL getViewExperimentModificationsURL(int experimentAnnotati @RequiresLogin public class RequestExtensionAction extends ConfirmAction { - private ExperimentAnnotations _exptAnnotations; private DatasetStatus _datasetStatus; @@ -10087,7 +10100,7 @@ public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException erro HtmlView view = new HtmlView(DIV( DIV("You are requesting an extension for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), DIV("Title: " + _exptAnnotations.getTitle()), - DIV("Submitted on: " + _exptAnnotations.getCreated()), + DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), DIV("Submitter: " + _exptAnnotations.getSubmitterName()) )); view.setTitle("Request Extension"); @@ -10105,12 +10118,13 @@ public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) ShortURLRecord shortUrl = _exptAnnotations.getShortUrl(); _datasetStatus = DatasetStatusManager.getForShortUrl(shortUrl); + PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); if (_datasetStatus != null) { - if (_datasetStatus.isExtensionValid()) + if (_datasetStatus.isExtensionValid(settings)) { errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + shortUrl.renderShortURL() - + ". The extension is valid until " + _datasetStatus.extensionValidUntil()); + + ". The extension is valid until " + _datasetStatus.extensionValidUntilFormatted(settings)); } else if (_datasetStatus.deletionRequested()) { @@ -10138,6 +10152,8 @@ public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throw DatasetStatusManager.update(datasetStatus, getUser()); } + _datasetStatus = datasetStatus; + // Post a message to the support thread. JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(_exptAnnotations); Journal journal = JournalManager.getJournal(submission.getJournalId()); @@ -10152,8 +10168,14 @@ public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throw @Override public ModelAndView getSuccessView(ShortUrlForm shortUrlForm) { + PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); return new HtmlView(DIV("An extension request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(), - DIV("Extension is valid until " + _datasetStatus.extensionValidUntil()))); + DIV("The extension is valid until " + _datasetStatus.extensionValidUntilFormatted(settings)), + BR(), + DIV( + LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer())) + ) + )); } @Override @@ -10175,9 +10197,9 @@ public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException erro { setTitle("Request Deletion For Panorama Public Data"); HtmlView view = new HtmlView(DIV( - DIV("You are requesting deletion for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), + DIV("You are requesting deletion of the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), DIV("Title: " + _exptAnnotations.getTitle()), - DIV("Submitted on: " + _exptAnnotations.getCreated()), + DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), DIV("Submitter: " + _exptAnnotations.getSubmitterName()) )); view.setTitle("Request Deletion"); @@ -10199,7 +10221,7 @@ public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) { if (_datasetStatus.deletionRequested()) { - errors.reject(ERROR_MSG, "A deletion request was already submitted on " + _datasetStatus.getDeletionRequestedDate() + " for the data with short URL " + shortUrl.renderShortURL()); + errors.reject(ERROR_MSG, "A deletion request was already submitted on " + _datasetStatus.getDeletionRequestedDateFormatted() + " for the data with short URL " + shortUrl.renderShortURL()); } } } @@ -10237,8 +10259,10 @@ public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throw @Override public ModelAndView getSuccessView(ShortUrlForm shortUrlForm) { - return new HtmlView(DIV("An extension request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(), - DIV("Extension is valid until " + _datasetStatus.extensionValidUntil()))); + return new HtmlView(DIV("A deletion request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(), + BR(), + DIV(new ButtonBuilder("Home").submit(false).href(AppProps.getInstance().getHomePageActionURL())) + )); } @Override @@ -10287,7 +10311,7 @@ private static ExperimentAnnotations getValidExperimentAnnotations(ShortUrlForm } // User requesting the extension / deletion must be the data submitter or lab head - if (!(user.equals(exptAnnotations.getSubmitterUser()) && user.equals(exptAnnotations.getLabHeadUser()))) + if (!(user.equals(exptAnnotations.getSubmitterUser()) || user.equals(exptAnnotations.getLabHeadUser()))) { errors.reject(ERROR_MSG, "Status change can be requested only by the data submitter or lab head."); return null; @@ -10304,36 +10328,51 @@ private static ExperimentAnnotations getValidExperimentAnnotations(ShortUrlForm @RequiresPermission(AdminOperationsPermission.class) - public static class PrivateDataReminderSettingsAction extends FormViewAction + public static class PrivateDataReminderSettingsAction extends FormViewAction { @Override - public void validateCommand(PrivateDataReminderSettings target, Errors errors) - { - - } + public void validateCommand(PrivateDataReminderSettingsForm form, Errors errors) {} @Override - public ModelAndView getView(PrivateDataReminderSettings privateDataReminderSettings, boolean reshow, BindException errors) throws Exception + public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow, BindException errors) throws Exception { - ActionURL postRemindersUrl = null; + PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + VBox view = new VBox(); view.addView(new HtmlView( DIV( - "Posts a reminder to the support message threads of the still-private datasets on Panorama Public.", + ERRORS(errors), + "Posts a reminder to the support message threads of the private datasets on Panorama Public.", DIV( - CHECKBOX(at(name, "enabled")), - "Send private data reminders", - BR(), - new ButtonBuilder("Save").submit(true).build() + FORM(at(method, "POST", action, new ActionURL(PrivateDataReminderSettingsAction.class, getContainer())), + TABLE( + TR( + TD(cl("labkey-form-label"), SPAN(PrivateDataMessageSettings.PROP_ENABLE_REMINDER)), + TD(at(style, "padding:0 10px 0 0;"), INPUT(at(type, "checkbox", name, "enabled", checked, settings.isEnableReminders()))) + ), + TR( + TD(cl("labkey-form-label"), SPAN(PrivateDataMessageSettings.PROP_EXTENSION_MONTHS)), + TD(at(style, "padding:0 10px 0 0;"), INPUT(at(type, "Text", name, "extensionLength", value, settings.getExtensionLength()))) + ), + TR( + TD(cl("labkey-form-label"), SPAN(PrivateDataMessageSettings.PROP_REMINDER_FREQUENCY)), + TD(at(style, "padding:0 10px 0 0;"), INPUT(at(type, "Text", name, "reminderFrequency", value, settings.getReminderFrequency()))) + ) + ), + new ButtonBuilder("Save").submit(true).build() + ) ), HR(), DIV( - LinkBuilder.labkeyLink("Send Reminders Now") - .usePost("Are you sure you want to post reminder messages for private datasets?") - .href(postRemindersUrl).build(), - BR(), - CHECKBOX(at(name, "test")), - "Test Mode" + FORM(at(method, "POST", action, new ActionURL(SendPrivateDataRemindersAction.class, getContainer())), + new ButtonBuilder("Send Reminders Now") + .usePost("Are you sure you want to send reminder messages for private datasets?") + .submit(true) + .build(), + HtmlString.NBSP, + CHECKBOX(at(name, "testMode", checked, false)), + "Test Mode" + ) ) ) )); @@ -10343,17 +10382,34 @@ public ModelAndView getView(PrivateDataReminderSettings privateDataReminderSetti } @Override - public boolean handlePost(PrivateDataReminderSettings privateDataReminderSettings, BindException errors) throws Exception + public boolean handlePost(PrivateDataReminderSettingsForm form, BindException errors) throws Exception { - return false; + PrivateDataMessageSettings settings = new PrivateDataMessageSettings(); + settings.setEnableReminders(form.isEnabled()); + settings.setExtensionLength(form.getExtensionLength()); + settings.setReminderFrequency(form.getReminderFrequency()); + PrivateDataMessageSettings.save(settings); + + PrivateDataMessageScheduler.getInstance().initialize(settings.isEnableReminders()); + return true; } @Override - public URLHelper getSuccessURL(PrivateDataReminderSettings privateDataReminderSettings) + public URLHelper getSuccessURL(PrivateDataReminderSettingsForm privateDataReminderSettingsForm) { return null; } + @Override + public ModelAndView getSuccessView(PrivateDataReminderSettingsForm form) + { + ActionURL adminUrl = new ActionURL(PanoramaPublicAdminViewAction.class, getContainer()); + return new HtmlView( + DIV("Private data message settings saved!", + BR(), + new LinkBuilder("Back to Panorama Public Admin Console").href(adminUrl).build())); + } + @Override public void addNavTrail(NavTree root) { @@ -10361,10 +10417,11 @@ public void addNavTrail(NavTree root) } } - private static class PrivateDataReminderSettings + private static class PrivateDataReminderSettingsForm { private boolean _enabled; - private boolean _test; + private int _extensionLength; + private int _reminderFrequency; public boolean isEnabled() { @@ -10376,17 +10433,68 @@ public void setEnabled(boolean enabled) _enabled = enabled; } - public boolean isTest() + public int getExtensionLength() { - return _test; + return _extensionLength; } - public void setTest(boolean test) + public void setExtensionLength(int extensionLength) { - _test = test; + _extensionLength = extensionLength; + } + + public int getReminderFrequency() + { + return _reminderFrequency; + } + + public void setReminderFrequency(int reminderFrequency) + { + _reminderFrequency = reminderFrequency; + } + } + + @RequiresPermission(AdminOperationsPermission.class) + public class SendPrivateDataRemindersAction extends FormHandlerAction + { + @Override + public void validateCommand(PrivateDataSendReminderForm form, Errors errors) + { + + } + + @Override + public boolean handlePost(PrivateDataSendReminderForm form, BindException errors) throws Exception + { + //List selectedExperimentIds = form.getSelectedExperimentIds(); + PipelineJob job = new PrivateDataReminderJob(getViewBackgroundInfo(), + PipelineService.get().getPipelineRootSetting(ContainerManager.getRoot()), + form.isTestMode()); + PipelineService.get().queueJob(job); + return true; + } + + @Override + public URLHelper getSuccessURL(PrivateDataSendReminderForm form) + { + return PageFlowUtil.urlProvider(PipelineUrls.class).urlBegin(getContainer()); } } + private static class PrivateDataSendReminderForm + { + private boolean _testMode; + + public boolean isTestMode() + { + return _testMode; + } + + public void setTestMode(boolean testMode) + { + _testMode = testMode; + } + } public static ActionURL getCopyExperimentURL(int experimentAnnotationsId, int journalId, Container container) { diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java index 34db3a86..8809ea6b 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -15,6 +15,7 @@ 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; @@ -24,8 +25,8 @@ 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.model.validation.Status; import org.labkey.panoramapublic.proteomexchange.ProteomeXchangeService; import org.labkey.panoramapublic.query.ExperimentAnnotationsManager; import org.labkey.panoramapublic.query.JournalManager; @@ -307,13 +308,14 @@ public static void postPrivateStatusExtensionMessage(@NotNull Journal journal, @ Your data has been granted an extension for an additional 6 months. You’ll receive another reminder at that time, or you may make the dataset public earlier. Please feel free to contact us if you have any questions */ - String messageTitle = "Private Status Extension" +" - " + je.getShortAccessUrl().renderShortURL(); + 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 an extension for an additional " + DatasetStatus.EXTENSION_VALID_MONTHS + " months. ") - .append("You will receive another reminder at that time, or you may make the data public earlier ") - .append("by clicking the \"Make Public\" button in your data folder or by clicking this link: ") + .append("Your data has been granted a " + DatasetStatus.EXTENSION_VALID_MONTHS + " month extension. ") + .append("You’ll 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,"); @@ -349,7 +351,7 @@ public static void postDataDeletionRequestMessage(@NotNull Journal journal, @Not 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 already deleted."); + .append("may have been deleted."); } messageBody.append(NL2).append("Best regards,"); @@ -359,43 +361,76 @@ public static void postDataDeletionRequestMessage(@NotNull Journal journal, @Not } - public static void postPrivateDataReminderMessage(@NotNull Journal journal, @NotNull JournalExperiment je, @NotNull ExperimentAnnotations expAnnotations, - @NotNull User submitter, @NotNull User messagePoster, List notifyUsers) + 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) { - ExperimentAnnotations sourceExperiment = ExperimentAnnotationsManager.get(expAnnotations.getSourceExperimentId()); - String message = getDataStatusReminderMessage(expAnnotations, sourceExperiment); + String message = getDataStatusReminderMessage(expAnnotations, submitter, js, announcement, announcementsContainer, journalAdmin); String title = "Action Required: Status Update for Your Private Dataset on Panorama Public"; - postNotificationFullTitle(journal, je, message, messagePoster, title, StatusOption.Closed, notifyUsers); + postNotificationFullTitle(journal, js.getJournalExperiment(), message, messagePoster, title, StatusOption.Closed, notifyUsers); } - public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations exptAnnotations, ExperimentAnnotations sourceExperiment) + public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations exptAnnotations, @NotNull User submitter, + @NotNull JournalSubmission js,@NotNull Announcement announcement, + @NotNull Container announcementContainer, @NotNull User journalAdmin) { /* - We hope you are doing well. We’re reaching out regarding your dataset on Panorama Public (https://panoramaweb.org/polyjuice.url), which has been private since January 1, 2024. + We’re reaching out regarding your dataset on Panorama Public (https://panoramaweb.org/polyjuice.url), which has been private since January 1, 2024. Is the paper associated with this work already published? If yes: Please make your data public by clicking the "Make Public" button in your folder or by clicking [Make Data Public] here. This helps ensure that your valuable research is easily accessible to the community. If not: You have a couple of options: - Request an Extension - If your paper is still under review, or you need additional time to publish, please let us know by clicking [Request Extension] + Request an Extension - If your paper is still under review, or you need additional time, please let us know by clicking [Request Extension] Delete from Panorama Public - If you no longer wish to host your data on Panorama Public, please click [Request Deletion]. We will remove your dataset from Panorama Public. However, your source folder (/Hogwarts/Gryffindor/magic-potion) will remain intact, allowing you to resubmit your data in the future if you wish. If you have any questions or need further assistance, please do not hesitate to respond to this message by clicking here. - Thank you for sharing your research on Panorama Public. We appreciate your commitment to open science and supporting the research community. + Thank you for sharing your research on Panorama Public. We appreciate your commitment to open science and your contributions to the research community. */ String shortUrl = exptAnnotations.getShortUrl().renderShortURL(); String makePublicLink = PanoramaPublicController.getMakePublicUrl(exptAnnotations.getId(), exptAnnotations.getContainer()).getURIString(); + String dateString = DateUtil.formatDateTime(js.getLatestSubmission().getCreated(), "MMMM d, yyyy"); + + 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("We hope you are doing well. ") - .append("We’re reaching out regarding your dataset on Panorama Public (").append(shortUrl).append("), which has been private since PLACEHOLDER_DATA_SUBMISSION_DATE.") + message.append("Dear ").append(getUserName(submitter)).append(",").append(NL2) + .append("We are reaching out regarding your dataset 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 [**Make Data Public**](").append(makePublicLink).append(")") + .append("\n- If yes: Please make your data public by clicking the \"Make Public\" button in your folder or by clicking ") + .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 to publish, please let us know by clicking [**Request Extension**]().") - .append("\n - **Delete from Panorama Public** - If you no longer wish to host your data on Panorama Public, please click [**Request Deletion**](). ") - .append("We will remove your dataset from Panorama Public. ") - .append("However, your source folder ([/Hogwarts/Gryffindor/magic-potion](http://localhost:8080/labkey/Hogwarts/Gryffindor/magic-potion/project-begin.view)) will remain intact, ") - .append("allowing you to resubmit your data in the future if you wish.") - .append("\n\nIf you have any questions or need further assistance, please do not hesitate to respond to this message by [**clicking here**](__PH__RESPOND__TO__MESSAGE__URL__).") - .append("\n\nThank you for sharing your research on Panorama Public. We appreciate your commitment to open science and supporting the research community."); + .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 dataset 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(); } @@ -422,7 +457,6 @@ this data (available at __PH__DATA__SHORT__URL__), we ask that you do so as soon public static String PLACEHOLDER_MAKE_DATA_PUBLIC_URL = PLACEHOLDER + "MAKE__DATA__PUBLIC__URL__"; public static String PLACEHOLDER_SHORT_URL = PLACEHOLDER + "DATA__SHORT__URL__"; - private static String PLACEHOLDER_DATA_SUBMISSION_DATE = PLACEHOLDER + "DATA__SUBMISSION__DATE__"; public static String replaceLinkPlaceholders(@NotNull String text, @NotNull ExperimentAnnotations expAnnotations, @NotNull Announcement announcement, @NotNull Container announcementContainer) { diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java index ae586276..53448951 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java @@ -1,6 +1,5 @@ package org.labkey.panoramapublic.message; -import org.apache.commons.lang3.math.NumberUtils; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; @@ -13,10 +12,7 @@ import org.labkey.api.util.ConfigurationException; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ViewBackgroundInfo; -import org.labkey.panoramapublic.catalog.CatalogEntrySettings; import org.labkey.panoramapublic.pipeline.PrivateDataReminderJob; -import org.labkey.panoramapublic.query.CatalogEntryManager; -import org.quartz.CronScheduleBuilder; import org.quartz.DateBuilder; import org.quartz.Job; import org.quartz.JobBuilder; @@ -27,19 +23,17 @@ import org.quartz.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; import org.quartz.impl.StdSchedulerFactory; -import java.util.Map; - -import static org.labkey.api.targetedms.TargetedMSService.MODULE_NAME; -import static org.labkey.api.targetedms.TargetedMSService.PROP_CHROM_LIB_REVISION; - public class PrivateDataMessageScheduler { private static final Logger _log = LogHelper.getLogger(PrivateDataMessageScheduler.class, "Panorama Public private data reminder message scheduler"); - private static String PROP_PRIVATE_DATA_REMINDER = "Panorama Public private data reminder"; - private static String PROP_ENABLE_REMINDER = "Enable private data reminder"; + public static String PROP_PRIVATE_DATA_REMINDER = "Panorama Public private data reminder"; + public static String PROP_ENABLE_REMINDER = "Enable private data reminder"; + + private static final TriggerKey TRIGGER_KEY = new TriggerKey(PrivateDataMessageScheduler.class.getCanonicalName()); private static final PrivateDataMessageScheduler _instance = new PrivateDataMessageScheduler(); @@ -50,24 +44,30 @@ public static PrivateDataMessageScheduler getInstance() private PrivateDataMessageScheduler(){} - public void initializeTimer() + public void initialize(boolean enable) { try { Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); - // Get configured quartz Trigger + // Clear previous job, if present + if (scheduler.checkExists(TRIGGER_KEY)) + scheduler.unscheduleJob(TRIGGER_KEY); + + if (!enable) + { + return; + } + + // Get the quartz Trigger Trigger trigger = getTrigger(); - // Create a quartz job that invokes a pipeline job that posts private data reminder messages + // Create a quartz job that queues a pipeline job that posts private data reminder messages JobDetail job = JobBuilder.newJob(PrivateDataMessageSchedulerJob.class) - .withIdentity(PrivateDataMessageSchedulerJob.class.getCanonicalName()) + .withIdentity(PrivateDataMessageScheduler.class.getCanonicalName()) .build(); - // TODO: Add this PrivateDataMessageScheduler instance to the Job context so the Job knows which digest to send - // job.getJobDataMap().put(MESSAGE_SCHEDULER_KEY, this); - - // Schedule trigger to execute the message digest job on the configured schedule + // Schedule trigger to send reminders on the configured schedule scheduler.scheduleJob(job, trigger); } catch (SchedulerException e) @@ -79,13 +79,15 @@ public void initializeTimer() protected Trigger getTrigger() { // 1st of every month at 8:00AM - return TriggerBuilder.newTrigger() - .withSchedule(CronScheduleBuilder.monthlyOnDayAndHourAndMinute(1, 8, 0)) - .build(); // return TriggerBuilder.newTrigger() -// .withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(1)) -// .startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND)) +// .withIdentity(TRIGGER_KEY) +// .withSchedule(CronScheduleBuilder.monthlyOnDayAndHourAndMinute(1, 8, 0)) // .build(); + return TriggerBuilder.newTrigger() + .withIdentity(TRIGGER_KEY) + .withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(2)) + .startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND)) + .build(); } public static class PrivateDataMessageSchedulerJob implements Job diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java new file mode 100644 index 00000000..53e52c82 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java @@ -0,0 +1,83 @@ +package org.labkey.panoramapublic.message; + +import org.labkey.api.data.PropertyManager; + +public class PrivateDataMessageSettings +{ + public static String PROP_PRIVATE_DATA_REMINDER = "Panorama Public private data reminder settings"; + public static String PROP_ENABLE_REMINDER = "Enable private data reminder"; + public static String PROP_EXTENSION_MONTHS = "Extension duration (months)"; + public static String PROP_REMINDER_FREQUENCY = "Reminder frequency (months)"; + + + private static boolean DEFAULT_ENABLE_REMINDERS = false; + private static int DEFAULT_EXTENSION_LENGTH = 6; // Private status of a dataset can be extended by 6 months. + private static int DEFAULT_REMINDER_FREQUENCY = 1; // Send reminders once a month, unless extension or deletion was requested. + + private boolean _enableReminders; + private int _extensionLength; + private int _reminderFrequency; + + public static PrivateDataMessageSettings get() + { + PropertyManager.WritablePropertyMap settingsMap = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, false); + + PrivateDataMessageSettings settings = new PrivateDataMessageSettings(); + if(settingsMap != null) + { + boolean enableReminders = settingsMap.get(PROP_EXTENSION_MONTHS) == null ? DEFAULT_ENABLE_REMINDERS : Boolean.valueOf(settingsMap.get(PROP_ENABLE_REMINDER)); + settings.setEnableReminders(enableReminders); + int extensionLength = settingsMap.get(PROP_EXTENSION_MONTHS) == null ? DEFAULT_EXTENSION_LENGTH : Integer.valueOf(settingsMap.get(PROP_EXTENSION_MONTHS)); + settings.setExtensionLength(extensionLength); + int reminderFrequency = settingsMap.get(PROP_REMINDER_FREQUENCY) == null ? DEFAULT_EXTENSION_LENGTH : Integer.valueOf(settingsMap.get(PROP_REMINDER_FREQUENCY)); + settings.setReminderFrequency(reminderFrequency); + } + else + { + settings.setEnableReminders(DEFAULT_ENABLE_REMINDERS); + settings.setExtensionLength(DEFAULT_EXTENSION_LENGTH); + settings.setReminderFrequency(DEFAULT_REMINDER_FREQUENCY); + } + + return settings; + } + + public static void save(PrivateDataMessageSettings settings) + { + PropertyManager.WritablePropertyMap settingsMap = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, true); + settingsMap.put(PROP_ENABLE_REMINDER, String.valueOf(settings.isEnableReminders())); + settingsMap.put(PROP_EXTENSION_MONTHS, String.valueOf(settings.getExtensionLength())); + settingsMap.put(PROP_REMINDER_FREQUENCY, String.valueOf(settings.getReminderFrequency())); + settingsMap.save(); + } + + public void setEnableReminders(boolean enableReminders) + { + _enableReminders = enableReminders; + } + + public void setExtensionLength(int extensionLength) + { + _extensionLength = extensionLength; + } + + public void setReminderFrequency(int reminderFrequency) + { + _reminderFrequency = reminderFrequency; + } + + public boolean isEnableReminders() + { + return _enableReminders; + } + + public int getExtensionLength() + { + return _extensionLength; + } + + public int getReminderFrequency() + { + return _reminderFrequency; + } +} diff --git a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java index d6fa1ced..2d5b13e0 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -1,7 +1,9 @@ package org.labkey.panoramapublic.model; import org.jetbrains.annotations.Nullable; +import org.labkey.api.util.DateUtil; import org.labkey.api.view.ShortURLRecord; +import org.labkey.panoramapublic.message.PrivateDataMessageSettings; import java.time.ZoneId; import java.util.Date; @@ -51,6 +53,11 @@ public Date getDeletionRequestedDate() return _deletionRequestedDate; } + public @Nullable String getDeletionRequestedDateFormatted() + { + return format(_deletionRequestedDate); + } + public void setDeletionRequestedDate(Date deletionRequestedDate) { _deletionRequestedDate = deletionRequestedDate; @@ -61,7 +68,7 @@ public boolean deletionRequested() return _deletionRequestedDate != null; } - public boolean isExtensionValid() + public boolean isExtensionValid(PrivateDataMessageSettings settings) { if (_extensionRequestedDate == null) { @@ -72,12 +79,12 @@ public boolean isExtensionValid() .atZone(ZoneId.systemDefault()) .toLocalDate(); - LocalDate extensionValidStartDate = LocalDate.now().minusMonths(EXTENSION_VALID_MONTHS); + LocalDate extensionValidStartDate = LocalDate.now().minusMonths(settings.getExtensionLength()); return extensionDate.isAfter(extensionValidStartDate); } - public boolean isLastReminderRecent() + public boolean isLastReminderRecent(PrivateDataMessageSettings settings) { if (_lastReminderDate == null) { @@ -88,12 +95,12 @@ public boolean isLastReminderRecent() .atZone(ZoneId.systemDefault()) .toLocalDate(); - LocalDate extensionValidStartDate = LocalDate.now().minusMonths(EXTENSION_VALID_MONTHS); + LocalDate extensionValidStartDate = LocalDate.now().minusMonths(settings.getReminderFrequency()); return reminderDate.isAfter(extensionValidStartDate); } - public @Nullable Date extensionValidUntil() + public @Nullable Date extensionValidUntil(PrivateDataMessageSettings settings) { if (_extensionRequestedDate == null) { @@ -103,8 +110,18 @@ public boolean isLastReminderRecent() return Date.from( _extensionRequestedDate.toInstant() .atZone(ZoneId.systemDefault()) - .plusMonths(EXTENSION_VALID_MONTHS) + .plusMonths(settings.getExtensionLength()) .toInstant() ); } + + public @Nullable String extensionValidUntilFormatted(PrivateDataMessageSettings settings) + { + return format(extensionValidUntil(settings)); + } + + private @Nullable String format(Date date) + { + return date != null ? DateUtil.formatDateTime(date, "MMMM d, yyyy") : null; + } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index e095f09e..f34070be 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -9,12 +9,15 @@ 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.URLHelper; import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.panoramapublic.PanoramaPublicManager; import org.labkey.panoramapublic.PanoramaPublicNotification; +import org.labkey.panoramapublic.message.PrivateDataMessageSettings; import org.labkey.panoramapublic.model.DatasetStatus; import org.labkey.panoramapublic.model.ExperimentAnnotations; import org.labkey.panoramapublic.model.Journal; @@ -24,7 +27,9 @@ import org.labkey.panoramapublic.query.JournalManager; import org.labkey.panoramapublic.query.SubmissionManager; +import java.time.Instant; import java.util.ArrayList; +import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -67,11 +72,12 @@ private List getPrivateDatasets(Container projectFolder) { Set subFolders = ContainerManager.getAllChildren(projectFolder); List privateDataIds = new ArrayList<>(); + PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); for (Container folder: subFolders) { ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder); - if (shouldPostReminder(exptAnnotations)) + if (shouldPostReminder(exptAnnotations, settings)) { privateDataIds.add(exptAnnotations.getId()); } @@ -80,7 +86,7 @@ private List getPrivateDatasets(Container projectFolder) return privateDataIds; } - private boolean shouldPostReminder(ExperimentAnnotations exptAnnotations) + private boolean shouldPostReminder(ExperimentAnnotations exptAnnotations, PrivateDataMessageSettings settings) { if (exptAnnotations == null) return false; if (exptAnnotations.isPublic()) return false; @@ -92,22 +98,26 @@ private boolean shouldPostReminder(ExperimentAnnotations exptAnnotations) if (datasetStatus.deletionRequested()) return false; // Return false if the submitter has requested an extension, and the extension is still valid - if (datasetStatus.isExtensionValid()) return false; + if (datasetStatus.isExtensionValid(settings)) return false; + + // Return false if this is not the latest version of the experiment + if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) return false; // Returns false if the last reminder was sent less than a month ago - if (datasetStatus.isLastReminderRecent()) return false; + if (datasetStatus.isLastReminderRecent(settings)) return false; return true; } private void postMessage(List expAnnotationIds, Journal panoramaPublic) { - if (expAnnotationIds.size() == 0) + int total = expAnnotationIds.size(); + if (total == 0) { getLogger().info("No private datasets were found."); return; } - getLogger().info(String.format("%sPosting reminder message to: %d message threads", _test ? "TEST MODE: " : "", expAnnotationIds.size())); + getLogger().info(String.format("Posting reminder message to: %d message threads", expAnnotationIds.size())); int done = 0; @@ -119,11 +129,27 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) List submitterNotFound = new ArrayList<>(); Container announcementsFolder = panoramaPublic.getSupportContainer(); + if (announcementsFolder == null) + { + getLogger().error(String.format("%s does not have a support folder for messages.", panoramaPublic.getName())); + return; + } + + User journalAdmin = JournalManager.getJournalAdminUser(panoramaPublic); + if (journalAdmin == null) + { + getLogger().error(String.format("Could not find an admin user for %s.", panoramaPublic.getName())); + return; + } Set exptIds = new HashSet<>(expAnnotationIds); try (DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction()) { + if (_test) + { + getLogger().info("RUNNING IN TEST MODE - MESSAGES WILL NOT BE POSTED."); + } for (Integer experimentAnnotationsId : exptIds) { ExperimentAnnotations expAnnotations = ExperimentAnnotationsManager.get(experimentAnnotationsId); @@ -167,12 +193,23 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) { notifyList.add(expAnnotations.getLabHeadUser()); } - PanoramaPublicNotification.postPrivateDataReminderMessage(panoramaPublic, submission.getJournalExperiment(), expAnnotations, submitter, getUser(), notifyList); + PanoramaPublicNotification.postPrivateDataReminderMessage(panoramaPublic, submission, expAnnotations, + submitter, getUser(), notifyList, announcement, announcementsFolder, journalAdmin); + + DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(expAnnotations.getShortUrl()); + if (datasetStatus == null) + { + datasetStatus = new DatasetStatus(); + datasetStatus.setShortUrl(expAnnotations.getShortUrl()); + } + datasetStatus.setLastReminderDate(Date.from(Instant.now())); + DatasetStatusManager.save(datasetStatus, getUser()); } - done++; - getLogger().info(String.format("%s to message thread for experiment Id %d, announcement Id %d. Done: %d", - _test ? "Would post" : "Posted", experimentAnnotationsId, announcement.getRowId(), done)); + getLogger().info(String.format("Experiment ID: %d; Announcement ID %d; Short URL: %s.", + experimentAnnotationsId, announcement.getRowId(), expAnnotations.getShortUrl().renderShortURL())); + getLogger().info(String.format("Folder: %s", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(expAnnotations.getContainer()).getURIString())); + getLogger().info(String.format("Completed: %d of %d", ++done, total)); } transaction.commit(); @@ -205,6 +242,6 @@ public URLHelper getStatusHref() @Override public String getDescription() { - return "Posts a message to announcement threads of the private datasets"; + return "Post private data reminder messages"; } } From 7fda7cdafb0205b226a8c68a9109805d66b61c93 Mon Sep 17 00:00:00 2001 From: vagisha Date: Thu, 7 Aug 2025 19:19:06 -0700 Subject: [PATCH 03/18] - Working on test - Made SendPrivateDataRemindersAction a FormViewAction - DatasetStatus columns can be accessed via the ExperimentAnnotations table - Added constructor to pass the Journal (e.g. Panorama Public) and a list of experiments to PrivateDataReminderJob --- .../resources/schemas/panoramapublic.xml | 40 +++++++ .../PanoramaPublicController.java | 104 ++++++++++++++--- .../PanoramaPublicNotification.java | 2 +- .../message/PrivateDataMessageScheduler.java | 25 +--- .../message/PrivateDataMessageSettings.java | 14 +-- .../panoramapublic/model/DatasetStatus.java | 10 ++ .../pipeline/PrivateDataReminderJob.java | 62 ++++++---- .../query/ExperimentAnnotationsTableInfo.java | 79 ++++++++++++- .../view/sendPrivateDataRemindersForm.jsp | 78 +++++++++++++ .../PanoramaPublicBaseTest.java | 16 +++ .../PanoramaPublicMakePublicTest.java | 17 --- .../PrivateDataReminderTest.java | 107 ++++++++++++++++++ 12 files changed, 464 insertions(+), 90 deletions(-) create mode 100644 panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp create mode 100644 panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java diff --git a/panoramapublic/resources/schemas/panoramapublic.xml b/panoramapublic/resources/schemas/panoramapublic.xml index 70a96e08..95be5aaf 100644 --- a/panoramapublic/resources/schemas/panoramapublic.xml +++ b/panoramapublic/resources/schemas/panoramapublic.xml @@ -832,4 +832,44 @@ + + + + true + + + true + + + + UserId + core + UsersData + + true + + + true + + + + UserId + core + UsersData + + true + + + + + EntityId + core + ShortUrl + + + + + + +
\ No newline at end of file diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 37c30f32..d38ccfec 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10338,6 +10338,8 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow { PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + Journal panoramaPublic = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); + VBox view = new VBox(); view.addView(new HtmlView( DIV( @@ -10364,15 +10366,9 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow ), HR(), DIV( - FORM(at(method, "POST", action, new ActionURL(SendPrivateDataRemindersAction.class, getContainer())), - new ButtonBuilder("Send Reminders Now") - .usePost("Are you sure you want to send reminder messages for private datasets?") - .submit(true) - .build(), - HtmlString.NBSP, - CHECKBOX(at(name, "testMode", checked, false)), - "Test Mode" - ) + panoramaPublic != null + ? (new LinkBuilder("Send Reminders Now").href(new ActionURL(SendPrivateDataRemindersAction.class,panoramaPublic.getProject())).build()) + : "Panorama Public does not exist on the server" ) ) )); @@ -10413,6 +10409,7 @@ public ModelAndView getSuccessView(PrivateDataReminderSettingsForm form) @Override public void addNavTrail(NavTree root) { + addPanoramaPublicAdminConsoleNav(root, getContainer()); root.addChild("Private Data Reminder Settings"); } } @@ -10455,37 +10452,75 @@ public void setReminderFrequency(int reminderFrequency) } @RequiresPermission(AdminOperationsPermission.class) - public class SendPrivateDataRemindersAction extends FormHandlerAction + public class SendPrivateDataRemindersAction extends FormViewAction { @Override - public void validateCommand(PrivateDataSendReminderForm form, Errors errors) + 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(), "ExperimentAnnotationsTable", "ExperimentAnnotations"); + 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 Public Datasets"); + + form.setDataRegionName(tableView.getDataRegionName()); + + JspView jspView = new JspView<>("/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp", form, errors); + return new VBox(jspView, tableView); } @Override public boolean handlePost(PrivateDataSendReminderForm form, BindException errors) throws Exception { - //List selectedExperimentIds = form.getSelectedExperimentIds(); + 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(ContainerManager.getRoot()), - form.isTestMode()); + 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) + { + addPanoramaPublicAdminConsoleNav(root, ContainerManager.getRoot()); + root.addChild("Send Private Data Reminders"); + } } - private static class PrivateDataSendReminderForm + public static class PrivateDataSendReminderForm { private boolean _testMode; + private String _selectedIds; + private String _dataRegionName = null; + private List _experiments = null; - public boolean isTestMode() + public boolean getTestMode() { return _testMode; } @@ -10494,6 +10529,45 @@ 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; + } + + public List getExperiments() + { + return _experiments; + } + + public void setExperiments(List experiments) + { + _experiments = experiments; + } } public static ActionURL getCopyExperimentURL(int experimentAnnotationsId, int journalId, Container container) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java index 8809ea6b..4314ad35 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -375,7 +375,7 @@ public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations @NotNull Container announcementContainer, @NotNull User journalAdmin) { /* - We’re reaching out regarding your dataset on Panorama Public (https://panoramaweb.org/polyjuice.url), which has been private since January 1, 2024. + We are reaching out regarding your dataset on Panorama Public (https://panoramaweb.org/polyjuice.url), which has been private since January 1, 2024. Is the paper associated with this work already published? If yes: Please make your data public by clicking the "Make Public" button in your folder or by clicking [Make Data Public] here. This helps ensure that your valuable research is easily accessible to the community. If not: You have a couple of options: diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java index 53448951..204fba37 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java @@ -4,7 +4,6 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; import org.labkey.api.data.ContainerManager; -import org.labkey.api.data.PropertyManager; import org.labkey.api.pipeline.PipeRoot; import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.pipeline.PipelineService; @@ -30,9 +29,6 @@ public class PrivateDataMessageScheduler { private static final Logger _log = LogHelper.getLogger(PrivateDataMessageScheduler.class, "Panorama Public private data reminder message scheduler"); - public static String PROP_PRIVATE_DATA_REMINDER = "Panorama Public private data reminder"; - public static String PROP_ENABLE_REMINDER = "Enable private data reminder"; - private static final TriggerKey TRIGGER_KEY = new TriggerKey(PrivateDataMessageScheduler.class.getCanonicalName()); private static final PrivateDataMessageScheduler _instance = new PrivateDataMessageScheduler(); @@ -119,34 +115,15 @@ public void execute(JobExecutionContext context) throw new ConfigurationException("No valid pipeline root found in the root container"); } - PipelineJob job = new PrivateDataReminderJob(vbi, PipelineService.get().getPipelineRootSetting(ContainerManager.getRoot()), false); PipelineService.get().queueJob(job); } catch(Exception e) { - _log.error("Error queuing PrivateDataReminderJob", e); // TODO: Anything else? + _log.error("Error queuing PrivateDataReminderJob", e); // ExceptionUtil.logExceptionToMothership(null, e); } } } - - public static boolean isPrivateDataReminderEnabled() - { - PropertyManager.PropertyMap map = PropertyManager.getProperties(PROP_PRIVATE_DATA_REMINDER); - return Boolean.parseBoolean(map != null ? map.getOrDefault(PROP_ENABLE_REMINDER, "false") : "false"); - } - - public static void enablePrivateDataReminder() - { - PropertyManager.WritablePropertyMap map = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, true); - map.put(PROP_ENABLE_REMINDER, Boolean.TRUE.toString()); - } - - public static void disablePrivateDataReminder() - { - PropertyManager.WritablePropertyMap map = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, true); - map.put(PROP_ENABLE_REMINDER, Boolean.FALSE.toString()); - } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java index 53e52c82..3d9a2877 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java @@ -4,15 +4,15 @@ public class PrivateDataMessageSettings { - public static String PROP_PRIVATE_DATA_REMINDER = "Panorama Public private data reminder settings"; - public static String PROP_ENABLE_REMINDER = "Enable private data reminder"; - public static String PROP_EXTENSION_MONTHS = "Extension duration (months)"; - public static String PROP_REMINDER_FREQUENCY = "Reminder frequency (months)"; + 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_EXTENSION_MONTHS = "Extension duration (months)"; + public static final String PROP_REMINDER_FREQUENCY = "Reminder frequency (months)"; - private static boolean DEFAULT_ENABLE_REMINDERS = false; - private static int DEFAULT_EXTENSION_LENGTH = 6; // Private status of a dataset can be extended by 6 months. - private static int DEFAULT_REMINDER_FREQUENCY = 1; // Send reminders once a month, unless extension or deletion was requested. + private static final boolean DEFAULT_ENABLE_REMINDERS = false; + private static final int DEFAULT_EXTENSION_LENGTH = 6; // Private status of a dataset can be extended by 6 months. + private static final int DEFAULT_REMINDER_FREQUENCY = 1; // Send reminders once a month, unless extension or deletion was requested. private boolean _enableReminders; private int _extensionLength; diff --git a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java index 2d5b13e0..e70ca455 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -68,6 +68,16 @@ public boolean deletionRequested() return _deletionRequestedDate != null; } + public boolean extensionRequested() + { + return _extensionRequestedDate != null; + } + + public boolean reminderSent() + { + return _lastReminderDate != null; + } + public boolean isExtensionValid(PrivateDataMessageSettings settings) { if (_extensionRequestedDate == null) diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index f34070be..3750b146 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -29,6 +29,7 @@ import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.List; @@ -37,43 +38,40 @@ 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.getRootNioPath().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-private-data-reminder", "log"))); + _panoramaPublic = panoramaPublic; + _experimentAnnotationsIds = experimentAnnotationsIds; _test = test; } - @Override - public void run() + private static Journal getPanoramaPublic() { - setStatus(TaskStatus.running); - - Journal panoramaPublic = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); - if (panoramaPublic == null) - { - getLogger().error("Panorama Public project does not exist"); - return; - } - - List privateDatasetIds = getPrivateDatasets(panoramaPublic.getProject()); - - postMessage(privateDatasetIds, panoramaPublic); - - setStatus(TaskStatus.complete); + return JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); } - private List getPrivateDatasets(Container projectFolder) + public static List getPrivateDatasets(Journal panoramaPublic) { - Set subFolders = ContainerManager.getAllChildren(projectFolder); + if (panoramaPublic == null) return Collections.emptyList(); + + Set subFolders = ContainerManager.getAllChildren(panoramaPublic.getProject()); List privateDataIds = new ArrayList<>(); PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); - for (Container folder: subFolders) + for (Container folder : subFolders) { ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder); @@ -86,11 +84,14 @@ private List getPrivateDatasets(Container projectFolder) return privateDataIds; } - private boolean shouldPostReminder(ExperimentAnnotations exptAnnotations, PrivateDataMessageSettings settings) + private static boolean shouldPostReminder(ExperimentAnnotations exptAnnotations, PrivateDataMessageSettings settings) { if (exptAnnotations == null) return false; if (exptAnnotations.isPublic()) return false; + // Return false if this is not the latest version of the experiment + if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) return false; + DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(exptAnnotations.getShortUrl()); if (datasetStatus == null) return true; @@ -100,13 +101,24 @@ private boolean shouldPostReminder(ExperimentAnnotations exptAnnotations, Privat // Return false if the submitter has requested an extension, and the extension is still valid if (datasetStatus.isExtensionValid(settings)) return false; - // Return false if this is not the latest version of the experiment - if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) return false; - // Returns false if the last reminder was sent less than a month ago - if (datasetStatus.isLastReminderRecent(settings)) return false; + return !datasetStatus.isLastReminderRecent(settings); + } - return true; + @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) diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java index b743b15e..5b36fbe3 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.shorturl AS DatasetStatus ") + .append(" FROM ").append(PanoramaPublicManager.getTableInfoDatasetStatus(), "status") + .append(" WHERE ") + .append(" status.shortUrl = ").append(ExprColumn.STR_TABLE_ALIAS).append(".shortUrl") + .append(") "); + ExprColumn col = new ExprColumn(this, "DatasetStatus", datasetStatusSql, JdbcType.VARCHAR); + col.setDescription("Dataset Status"); + col.setDisplayColumnFactory(DatasetStatusColumn::new); + + col.setFk(QueryForeignKey + .from(getUserSchema(), cf) + .to(PanoramaPublicSchema.TABLE_DATASET_STATUS, "shortUrl", 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,59 @@ public void renderGridCellContents(RenderContext ctx, HtmlWriter out) out.write(HtmlString.NBSP); } } + + public static class DatasetStatusColumn extends DataColumn + { + private final FieldKey ID_COL = new FieldKey(getColumnInfo().getFieldKey(), "id"); + + 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(ID_COL, Integer.class); + + // Get the experiment connected with this status Id. + Integer experimentId = ctx.get(FieldKey.fromParts("id"), 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(ID_COL); + } + } } 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..de260cd4 --- /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. +
+ + + + + + + + + +
Test Mode: + +
<%=button("Post Reminders").onClick("submitForm();")%>
+
+
+
\ 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..eda69041 --- /dev/null +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -0,0 +1,107 @@ +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.categories.External; +import org.labkey.test.categories.MacCossLabModules; +import org.labkey.test.components.panoramapublic.TargetedMsExperimentWebPart; + +@Category({External.class, MacCossLabModules.class}) +@BaseWebDriverTest.ClassTimeout(minutes = 5) +public class PrivateDataReminderTest extends PanoramaPublicBaseTest +{ + private static final String SKY_FILE_1 = "MRMer.zip"; + static final String SUBMITTER_2 = "submitter_2@panoramapublic.test"; + static final String SUBMITTER_3 = "submitter_3@panoramapublic.test"; + private static final String ADMIN_2 = "admin_2@panoramapublic.test"; + private static final String ADMIN_3 = "admin_3@panoramapublic.test"; + + @Test + public void testPrivateDataReminder() + { + String projectName = getProjectName(); + String folderName_1 = "Private Data 1"; + String targetFolder_1 = "Private Data 1 Copy"; + String experimentTitle_1 = "Test for private data message reminder - DATA ONE"; + log("Creating data 1 folder and copying to Panorama Public."); + String shortAccessUrl_1 = setupFolderSubmitAndCopy(projectName, folderName_1, targetFolder_1, experimentTitle_1, SUBMITTER, "One", ADMIN_USER, SKY_FILE_1); + + String folderName_2 = "Private Data 2"; + String targetFolder_2 = "Private Data 2 Copy"; + String experimentTitle_2 = "Test for private data message reminder - DATA TWO"; + log("Creating data 2 folder and copying to Panorama Public."); + String shortAccessUrl_2 = setupFolderSubmitAndCopy(projectName, folderName_2, targetFolder_2, experimentTitle_2, SUBMITTER_2, "Two", ADMIN_2, SKY_FILE_1); + + String folderName_3 = "Private Data 3"; + String targetFolder_3 = "Private Data 3 Copy"; + String experimentTitle_3 = "Test for private data message reminder - DATA THREE"; + log("Creating data 3 folder and copying to Panorama Public."); + String shortAccessUrl_3 = setupFolderSubmitAndCopy(projectName, folderName_3, targetFolder_3, experimentTitle_3, SUBMITTER_3, "Three", ADMIN_3, SKY_FILE_1); + + log("Making data 3 public."); + makePublic(projectName, folderName_3, SUBMITTER_3); + + log("Verifying data 1 is private"); + verifyIsPublicColumn(PANORAMA_PUBLIC, experimentTitle_1, false); + log("Verifying data 2 is private"); + verifyIsPublicColumn(PANORAMA_PUBLIC, experimentTitle_2, false); + log("Verifying data 3 is public"); + verifyIsPublicColumn(PANORAMA_PUBLIC, experimentTitle_3, true); + + log("Sending reminders"); + sendReminders(); + + portalHelper.enterAdminMode(); + + + goToProjectFolder(projectName, folderName_1); + portalHelper.clickWebpartMenuItem("Targeted MS Experiment ", true, "Support Messages"); + assertTextPresent("Title: Action Required: Status Update for Your Private Dataset on Panorama Public"); + + goToProjectFolder(projectName, folderName_2); + portalHelper.clickWebpartMenuItem("Targeted MS Experiment ", true, "Support Messages"); + assertTextPresent("Title: Action Required: Status Update for Your Private Dataset on Panorama Public"); + + goToProjectFolder(projectName, folderName_3); + portalHelper.clickWebpartMenuItem("Targeted MS Experiment ", true, "Support Messages"); + assertTextNotPresent("Title: Action Required: Status Update for Your Private Dataset on Panorama Public"); + } + + private void makePublic(String projectName, String folderName, String user) + { + if (isImpersonating()) + { + stopImpersonating(true); + } + goToProjectFolder(projectName, folderName); + impersonate(user); + goToDashboard(); + makeDataPublic(true); + stopImpersonating(); + } + + protected void sendReminders() + { + goToAdminConsole().goToSettingsSection(); + clickAndWait(Locator.linkWithText("Panorama Public")); + clickAndWait(Locator.linkWithText("Private Data Reminder Settings")); + // checkCheckbox(Locator.input("testMode")); + setFormElement(Locator.input("extensionLength"), "2"); + setFormElement(Locator.input("reminderFrequency"), "0"); + clickButton("Save"); + waitForText("Private data message settings saved"); + clickAndWait(Locator.linkWithText("Back to Panorama Public Admin Console")); + + clickAndWait(Locator.linkWithText("Private Data Reminder Settings")); + doAndWaitForPageToLoad(() -> + { + clickButton("Send Reminders Now"); + assertAlertContains("Are you sure you want to send reminder messages for private datasets?"); + dismissAllAlerts(); + }); + + waitForPipelineJobsToComplete(1, "Post private data reminder messages", false); + } +} From d1611388510d06a6786224ab5e63a0e709a8c493 Mon Sep 17 00:00:00 2001 From: vagisha Date: Sun, 10 Aug 2025 11:27:59 -0700 Subject: [PATCH 04/18] - Moved Private data reminder settings form to a JSP page. - "Panorama Public" journal folder can be selected in a drop-down. --- .../PanoramaPublicController.java | 46 ++----- .../panoramapublic/PanoramaPublicModule.java | 2 +- .../view/privateDataRemindersSettingsForm.jsp | 123 ++++++++++++++++++ 3 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index d38ccfec..5915b715 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10336,42 +10336,16 @@ public void validateCommand(PrivateDataReminderSettingsForm form, Errors errors) @Override public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow, BindException errors) throws Exception { - PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); - - Journal panoramaPublic = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); - + if (!reshow) + { + PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + form.setEnabled(settings.isEnableReminders()); + form.setExtensionLength(settings.getExtensionLength()); + form.setReminderFrequency(settings.getReminderFrequency()); + } + VBox view = new VBox(); - view.addView(new HtmlView( - DIV( - ERRORS(errors), - "Posts a reminder to the support message threads of the private datasets on Panorama Public.", - DIV( - FORM(at(method, "POST", action, new ActionURL(PrivateDataReminderSettingsAction.class, getContainer())), - TABLE( - TR( - TD(cl("labkey-form-label"), SPAN(PrivateDataMessageSettings.PROP_ENABLE_REMINDER)), - TD(at(style, "padding:0 10px 0 0;"), INPUT(at(type, "checkbox", name, "enabled", checked, settings.isEnableReminders()))) - ), - TR( - TD(cl("labkey-form-label"), SPAN(PrivateDataMessageSettings.PROP_EXTENSION_MONTHS)), - TD(at(style, "padding:0 10px 0 0;"), INPUT(at(type, "Text", name, "extensionLength", value, settings.getExtensionLength()))) - ), - TR( - TD(cl("labkey-form-label"), SPAN(PrivateDataMessageSettings.PROP_REMINDER_FREQUENCY)), - TD(at(style, "padding:0 10px 0 0;"), INPUT(at(type, "Text", name, "reminderFrequency", value, settings.getReminderFrequency()))) - ) - ), - new ButtonBuilder("Save").submit(true).build() - ) - ), - HR(), - DIV( - panoramaPublic != null - ? (new LinkBuilder("Send Reminders Now").href(new ActionURL(SendPrivateDataRemindersAction.class,panoramaPublic.getProject())).build()) - : "Panorama Public does not exist on the server" - ) - ) - )); + view.addView(new JspView<>("/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp", form)); view.setTitle("Private Data Reminder Settings"); view.setFrame(WebPartView.FrameType.PORTAL); return view; @@ -10414,7 +10388,7 @@ public void addNavTrail(NavTree root) } } - private static class PrivateDataReminderSettingsForm + public static class PrivateDataReminderSettingsForm { private boolean _enabled; private int _extensionLength; diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java index 7a563843..35a74a6c 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java @@ -360,7 +360,7 @@ public Set getSchemaNames() @Override public void startBackgroundThreads() { - PrivateDataMessageScheduler.getInstance().initializeTimer(); + // PrivateDataMessageScheduler.getInstance().initialize(); } @NotNull 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..d26ba528 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp @@ -0,0 +1,123 @@ +<% + /* + * 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 import="org.labkey.panoramapublic.message.PrivateDataMessageSettings" %> +<%@ 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 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(); + +%> + + + + + +
+ + + + + + + + + + + + + + + +
+ <%=h(PrivateDataMessageSettings.PROP_ENABLE_REMINDER)%> + + /> +
+ <%=h(PrivateDataMessageSettings.PROP_EXTENSION_MONTHS)%> + + +
+ <%=h(PrivateDataMessageSettings.PROP_REMINDER_FREQUENCY)%> + + +
+ <%=button("Save").submit(true)%> + <%=button("Cancel").href(panoramaPublicAdminUrl)%> +
+
+ : + + <%=link("Send Reminders Now").onClick("clickSendRemindersLink();").build()%> +
+
+ +
\ No newline at end of file From 36e0b33c24ff1e893799e5e2c21383e576da4a8c Mon Sep 17 00:00:00 2001 From: vagisha Date: Wed, 13 Aug 2025 12:53:27 -0700 Subject: [PATCH 05/18] - Replaced DATATIME with TIMESTAMP, and fixed index creation in sql script. - Fixed RequestExtensionAction and RequestDeletionAction. - Bumped schema cersion in PanoramaPublicModule. - PrivateDataReminderJob skips experiments that should be be getting reminders. - Fixed column name in DatasetStatusTableInfo. - Added more testing in PrivateDataReminderTest. --- .../panoramapublic-25.001-25.002.sql | 8 +- .../PanoramaPublicController.java | 215 +++++---- .../panoramapublic/PanoramaPublicModule.java | 3 +- .../panoramapublic/model/DatasetStatus.java | 6 +- .../pipeline/PrivateDataReminderJob.java | 84 +++- .../query/DatasetStatusTableInfo.java | 2 +- .../view/privateDataRemindersSettingsForm.jsp | 2 +- .../view/sendPrivateDataRemindersForm.jsp | 2 +- .../PrivateDataReminderTest.java | 440 ++++++++++++++++-- 9 files changed, 570 insertions(+), 192 deletions(-) diff --git a/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql b/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql index 0ce308f9..e22e7021 100644 --- a/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql +++ b/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.001-25.002.sql @@ -8,9 +8,9 @@ CREATE TABLE panoramapublic.DatasetStatus Modified TIMESTAMP, ShortUrl ENTITYID NOT NULL, - LastReminderDate DATETIME, - ExtensionRequestedDate DATETIME, - DeletionRequestedDate DATETIME, + LastReminderDate TIMESTAMP, + ExtensionRequestedDate TIMESTAMP, + DeletionRequestedDate TIMESTAMP, CONSTRAINT PK_DatasetStatus PRIMARY KEY (Id), @@ -18,4 +18,4 @@ CREATE TABLE panoramapublic.DatasetStatus CONSTRAINT UQ_DatasetStatus_ShortUrl UNIQUE (ShortUrl) ); -CREATE INDEX IX_DatasetStatus_ShortUrl ON panoramapublic.CatalogEntry(ShortUrl); +CREATE INDEX IX_DatasetStatus_ShortUrl ON panoramapublic.DatasetStatus(ShortUrl); diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 5915b715..7650d19f 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -78,7 +78,6 @@ import org.labkey.api.module.Module; import org.labkey.api.module.ModuleLoader; import org.labkey.api.module.ModuleProperty; -import org.labkey.api.module.SimpleAction; import org.labkey.api.pipeline.LocalDirectory; import org.labkey.api.pipeline.PipeRoot; import org.labkey.api.pipeline.PipelineJob; @@ -261,7 +260,6 @@ import static org.labkey.api.util.DOM.Attribute.valign; import static org.labkey.api.util.DOM.Attribute.value; import static org.labkey.api.util.DOM.Attribute.width; -import static org.labkey.api.util.DOM.LK.CHECKBOX; import static org.labkey.api.util.DOM.LK.ERRORS; import static org.labkey.api.util.DOM.LK.FORM; import static org.labkey.panoramapublic.proteomexchange.NcbiUtils.PUBMED_ID; @@ -10087,50 +10085,32 @@ public static ActionURL getViewExperimentModificationsURL(int experimentAnnotati return result; } - @RequiresLogin - public class RequestExtensionAction extends ConfirmAction + @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class}) + public abstract class UpdateDatasetStatusAction extends ConfirmAction { - private ExperimentAnnotations _exptAnnotations; - private DatasetStatus _datasetStatus; + protected ExperimentAnnotations _exptAnnotations; + protected DatasetStatus _datasetStatus; - @Override - public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception - { - setTitle("Request Extension For Panorama Public Data"); - HtmlView view = new HtmlView(DIV( - DIV("You are requesting an extension for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), - DIV("Title: " + _exptAnnotations.getTitle()), - DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), - DIV("Submitter: " + _exptAnnotations.getSubmitterName()) - )); - view.setTitle("Request Extension"); - return view; - } + protected abstract void doValidationForAction(Errors errors); + protected abstract void updateDatasetStatus(DatasetStatus datasetStatus); + protected abstract void postNotification() throws Exception; @Override public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) { - _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, getUser(), errors); + _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, errors); if (_exptAnnotations == null) { return; } + ensureCorrectContainer(getContainer(), _exptAnnotations.getContainer(), getViewContext()); + ShortURLRecord shortUrl = _exptAnnotations.getShortUrl(); _datasetStatus = DatasetStatusManager.getForShortUrl(shortUrl); - PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); - if (_datasetStatus != null) - { - if (_datasetStatus.isExtensionValid(settings)) - { - errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + shortUrl.renderShortURL() - + ". The extension is valid until " + _datasetStatus.extensionValidUntilFormatted(settings)); - } - else if (_datasetStatus.deletionRequested()) - { - errors.reject(ERROR_MSG, "A deletion request was submitted on " + _datasetStatus.getDeletionRequestedDate() + " for the data with short URL " + shortUrl.renderShortURL()); - } - } + + // Action-specific validation + doValidationForAction(errors); } @Override @@ -10139,25 +10119,23 @@ public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throw DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(_exptAnnotations.getShortUrl()); try(DbScope.Transaction transaction = CoreSchema.getInstance().getSchema().getScope().ensureTransaction()) { - if (datasetStatus == null) // TODO: Can this ever be null? + if (datasetStatus == null) { datasetStatus = new DatasetStatus(); datasetStatus.setShortUrl(_exptAnnotations.getShortUrl()); - datasetStatus.setExtensionRequestedDate(new Date()); + updateDatasetStatus(datasetStatus); DatasetStatusManager.save(datasetStatus, getUser()); } else { - datasetStatus.setExtensionRequestedDate(new Date()); + updateDatasetStatus(datasetStatus); DatasetStatusManager.update(datasetStatus, getUser()); } _datasetStatus = datasetStatus; - // 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()); + // Post notification + postNotification(); transaction.commit(); } @@ -10165,111 +10143,133 @@ public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throw 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 + public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception + { + setTitle("Request Extension"); + HtmlView view = new HtmlView(DIV( + DIV("You are requesting an extension for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), + DIV("Title: " + _exptAnnotations.getTitle()), + DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), + DIV("Submitter: " + _exptAnnotations.getSubmitterName()) + )); + view.setTitle("Request Extension For Panorama Public Data"); + return view; + } + + @Override + protected void doValidationForAction(Errors errors) + { + PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + if (_datasetStatus != null) + { + if (_datasetStatus.isExtensionCurrent(settings)) + { + errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + _datasetStatus.getShortUrl().renderShortURL() + + ". The extension is valid until " + _datasetStatus.extensionValidUntilFormatted(settings)); + } + else if (_datasetStatus.deletionRequested()) + { + errors.reject(ERROR_MSG, "A deletion request was submitted on " + _datasetStatus.getDeletionRequestedDate() + + " for the data with short URL " + _datasetStatus.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"); PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); return new HtmlView(DIV("An extension request was successfully submitted for the data at " + _exptAnnotations.getShortUrl().renderShortURL(), DIV("The extension is valid until " + _datasetStatus.extensionValidUntilFormatted(settings)), BR(), DIV( - LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer())) + LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer())) ) - )); - } - - @Override - public @NotNull URLHelper getSuccessURL(ShortUrlForm shortUrlForm) - { - return null; + )); } } - @RequiresLogin - public class RequestDeletionAction extends ConfirmAction + @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class}) + public class RequestDeletionAction extends UpdateDatasetStatusAction { - - private ExperimentAnnotations _exptAnnotations; - private DatasetStatus _datasetStatus; - @Override public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception { - setTitle("Request Deletion For Panorama Public Data"); + setTitle("Request Deletion"); HtmlView view = new HtmlView(DIV( DIV("You are requesting deletion of the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), DIV("Title: " + _exptAnnotations.getTitle()), DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), DIV("Submitter: " + _exptAnnotations.getSubmitterName()) )); - view.setTitle("Request Deletion"); + view.setTitle("Request Deletion For Panorama Public Data"); return view; } @Override - public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) + protected void doValidationForAction(Errors errors) { - _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, getUser(), errors); - if (_exptAnnotations == null) - { - return; - } - - ShortURLRecord shortUrl = _exptAnnotations.getShortUrl(); - _datasetStatus = DatasetStatusManager.getForShortUrl(shortUrl); 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 " + shortUrl.renderShortURL()); + errors.reject(ERROR_MSG, "A deletion request was already submitted on " + _datasetStatus.getDeletionRequestedDateFormatted() + + " for the data with short URL " + _datasetStatus.getShortUrl().renderShortURL()); } } } @Override - public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throws Exception + protected void updateDatasetStatus(DatasetStatus datasetStatus) { - DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(_exptAnnotations.getShortUrl()); - try(DbScope.Transaction transaction = CoreSchema.getInstance().getSchema().getScope().ensureTransaction()) - { - if (datasetStatus == null) // TODO: Can this ever be null? - { - datasetStatus = new DatasetStatus(); - datasetStatus.setShortUrl(_exptAnnotations.getShortUrl()); - datasetStatus.setDeletionRequestedDate(new Date()); - DatasetStatusManager.save(datasetStatus, getUser()); - } - else - { - datasetStatus.setDeletionRequestedDate(new Date()); - DatasetStatusManager.update(datasetStatus, getUser()); - } - - // 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()); - - transaction.commit(); - } + datasetStatus.setDeletionRequestedDate(new Date()); + } - return true; + @Override + protected void postNotification() throws Exception + { + // 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(new ButtonBuilder("Home").submit(false).href(AppProps.getInstance().getHomePageActionURL())) )); } - - @Override - public @NotNull URLHelper getSuccessURL(ShortUrlForm shortUrlForm) - { - return null; - } } public static class ShortUrlForm @@ -10287,7 +10287,7 @@ public void setShortUrlEntityId(String shortUrlEntityId) } } - private static ExperimentAnnotations getValidExperimentAnnotations(ShortUrlForm shortUrlForm, User user, Errors errors) + private static ExperimentAnnotations getValidExperimentAnnotations(ShortUrlForm shortUrlForm, Errors errors) { String shortUrlEntityId = shortUrlForm.getShortUrlEntityId(); if (StringUtils.isBlank(shortUrlEntityId)) @@ -10310,13 +10310,6 @@ private static ExperimentAnnotations getValidExperimentAnnotations(ShortUrlForm return null; } - // User requesting the extension / deletion must be the data submitter or lab head - if (!(user.equals(exptAnnotations.getSubmitterUser()) || user.equals(exptAnnotations.getLabHeadUser()))) - { - errors.reject(ERROR_MSG, "Status change can be requested only by the data submitter or lab head."); - return null; - } - if (exptAnnotations.isPublic()) { errors.reject(ERROR_MSG, "Data for short URL " + shortUrl.renderShortURL() + " is public. Status cannot be changed."); @@ -10327,6 +10320,7 @@ private static ExperimentAnnotations getValidExperimentAnnotations(ShortUrlForm } + @AdminConsoleAction @RequiresPermission(AdminOperationsPermission.class) public static class PrivateDataReminderSettingsAction extends FormViewAction { @@ -10343,7 +10337,7 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow form.setExtensionLength(settings.getExtensionLength()); form.setReminderFrequency(settings.getReminderFrequency()); } - + VBox view = new VBox(); view.addView(new JspView<>("/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp", form)); view.setTitle("Private Data Reminder Settings"); @@ -10383,7 +10377,7 @@ public ModelAndView getSuccessView(PrivateDataReminderSettingsForm form) @Override public void addNavTrail(NavTree root) { - addPanoramaPublicAdminConsoleNav(root, getContainer()); + PageFlowUtil.urlProvider(AdminUrls.class).addAdminNavTrail(root, "Panorama Public Admin Console", PanoramaPublicAdminViewAction.class, getContainer()); root.addChild("Private Data Reminder Settings"); } } @@ -10447,11 +10441,14 @@ public ModelAndView getView(PrivateDataSendReminderForm form, boolean reshow, Bi QueryView tableView = new QueryView(new PanoramaPublicSchema(getUser(), getContainer()), qSettings, null); tableView.setTitle("Private Panorama Public Datasets"); + tableView.setFrame(WebPartView.FrameType.NONE); form.setDataRegionName(tableView.getDataRegionName()); JspView jspView = new JspView<>("/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp", form, errors); - return new VBox(jspView, tableView); + VBox view = new VBox(jspView, tableView); + view.setFrame(WebPartView.FrameType.PORTAL); + return view; } @Override @@ -10482,7 +10479,7 @@ public URLHelper getSuccessURL(PrivateDataSendReminderForm form) @Override public void addNavTrail(NavTree root) { - addPanoramaPublicAdminConsoleNav(root, ContainerManager.getRoot()); + PageFlowUtil.urlProvider(AdminUrls.class).addAdminNavTrail(root, "Private Data Reminder Settings", PrivateDataReminderSettingsAction.class, ContainerManager.getRoot()); root.addChild("Send Private Data Reminders"); } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java index 35a74a6c..d12ff400 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java @@ -46,7 +46,6 @@ import org.labkey.panoramapublic.bluesky.BlueskyApiClient; import org.labkey.panoramapublic.bluesky.PanoramaPublicLogoResourceType; import org.labkey.panoramapublic.catalog.CatalogImageAttachmentType; -import org.labkey.panoramapublic.message.PrivateDataMessageScheduler; import org.labkey.panoramapublic.model.Journal; import org.labkey.panoramapublic.model.speclib.SpecLibKey; import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineProvider; @@ -92,7 +91,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 25.001; + return 25.002; } @Override diff --git a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java index e70ca455..785f31cd 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -78,7 +78,7 @@ public boolean reminderSent() return _lastReminderDate != null; } - public boolean isExtensionValid(PrivateDataMessageSettings settings) + public boolean isExtensionCurrent(PrivateDataMessageSettings settings) { if (_extensionRequestedDate == null) { @@ -105,9 +105,9 @@ public boolean isLastReminderRecent(PrivateDataMessageSettings settings) .atZone(ZoneId.systemDefault()) .toLocalDate(); - LocalDate extensionValidStartDate = LocalDate.now().minusMonths(settings.getReminderFrequency()); + LocalDate reminderValidStartDate = LocalDate.now().minusMonths(settings.getReminderFrequency()); - return reminderDate.isAfter(extensionValidStartDate); + return reminderDate.isAfter(reminderValidStartDate); } public @Nullable Date extensionValidUntil(PrivateDataMessageSettings settings) diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index 3750b146..b510b9f5 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -55,6 +55,7 @@ public PrivateDataReminderJob(ViewBackgroundInfo info, @NotNull PipeRoot root, J super("Panorama Public", info, root); setLogFile(root.getRootNioPath().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-private-data-reminder", "log"))); _panoramaPublic = panoramaPublic; + _experimentAnnotationsIds = experimentAnnotationsIds; _test = test; } @@ -74,35 +75,58 @@ public static List getPrivateDatasets(Journal panoramaPublic) for (Container folder : subFolders) { ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder); - - if (shouldPostReminder(exptAnnotations, settings)) - { - privateDataIds.add(exptAnnotations.getId()); - } + privateDataIds.add(exptAnnotations.getId()); } return privateDataIds; } - private static boolean shouldPostReminder(ExperimentAnnotations exptAnnotations, PrivateDataMessageSettings settings) + private static ReminderDecision getReminderDecision(ExperimentAnnotations exptAnnotations, PrivateDataMessageSettings settings) { - if (exptAnnotations == null) return false; - if (exptAnnotations.isPublic()) return false; + if (exptAnnotations == null) + return ReminderDecision.skip("Experiment annotations are null"); - // Return false if this is not the latest version of the experiment - if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) return false; + if (exptAnnotations.isPublic()) + return ReminderDecision.skip("Dataset is already public"); + + if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) + return ReminderDecision.skip("Not the current version of the experiment"); DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(exptAnnotations.getShortUrl()); - if (datasetStatus == null) return true; + if (datasetStatus == null) + return ReminderDecision.post(); + + if (datasetStatus.deletionRequested()) + return ReminderDecision.skip("Submitter has requested deletion"); - // Return false if the submitter has requested deletion - if (datasetStatus.deletionRequested()) return false; + if (datasetStatus.isExtensionCurrent(settings)) + return ReminderDecision.skip("Submitter requested an extension. Extension is current."); - // Return false if the submitter has requested an extension, and the extension is still valid - if (datasetStatus.isExtensionValid(settings)) return false; + if (datasetStatus.isLastReminderRecent(settings)) + return ReminderDecision.skip("Recent reminder already sent"); - // Returns false if the last reminder was sent less than a month ago - return !datasetStatus.isLastReminderRecent(settings); + 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 @@ -133,12 +157,15 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) int done = 0; + PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + AnnouncementService announcementSvc = AnnouncementService.get(); List experimentNotFound = new ArrayList<>(); List submissionNotFound = new ArrayList<>(); List announcementNotFound = new ArrayList<>(); List submitterNotFound = new ArrayList<>(); + int skipped = 0; Container announcementsFolder = panoramaPublic.getSupportContainer(); if (announcementsFolder == null) @@ -171,6 +198,14 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) experimentNotFound.add(experimentAnnotationsId); continue; } + + ReminderDecision decision = getReminderDecision(expAnnotations, settings); + if (!decision.shouldPost()) + { + getLogger().info("Skipping reminder for experiment Id " + experimentAnnotationsId + " - " + decision.getReason()); + skipped++; + continue; + } JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(expAnnotations); if (submission == null || submission.getLatestSubmission() == null) { @@ -213,9 +248,14 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) { datasetStatus = new DatasetStatus(); datasetStatus.setShortUrl(expAnnotations.getShortUrl()); + datasetStatus.setLastReminderDate(Date.from(Instant.now())); + DatasetStatusManager.save(datasetStatus, getUser()); + } + else + { + datasetStatus.setLastReminderDate(Date.from(Instant.now())); + DatasetStatusManager.update(datasetStatus, getUser()); } - datasetStatus.setLastReminderDate(Date.from(Instant.now())); - DatasetStatusManager.save(datasetStatus, getUser()); } getLogger().info(String.format("Experiment ID: %d; Announcement ID %d; Short URL: %s.", @@ -241,7 +281,11 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) } if (!submitterNotFound.isEmpty()) { - getLogger().error("Submitter user was not found for the following experiment Ids: " + StringUtils.join(submissionNotFound, ", ")); + getLogger().error("Submitter user was not found for the following experiment Ids: " + StringUtils.join(submitterNotFound, ", ")); + } + if (skipped > 0) + { + getLogger().info("Skipped posting reminders for " + skipped + " experiments "); } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java index a8fa8701..c72b58d1 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java @@ -41,7 +41,7 @@ public DatasetStatusTableInfo(@NotNull PanoramaPublicSchema userSchema, Containe visibleColumns.add(FieldKey.fromParts("ModifiedBy")); visibleColumns.add(FieldKey.fromParts("ShortUrl")); visibleColumns.add(FieldKey.fromParts("Title")); - visibleColumns.add(FieldKey.fromParts("ReminderDate")); + 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/view/privateDataRemindersSettingsForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp index d26ba528..f3624680 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp @@ -103,7 +103,7 @@
: - <% boolean isFirst = true; for (Journal journal : journals) { diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp index de260cd4..1161d874 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp @@ -68,7 +68,7 @@ Test Mode: - + /> <%=button("Post Reminders").onClick("submitForm();")%> diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index eda69041..c8872cce 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -2,106 +2,444 @@ import org.junit.Test; import org.junit.experimental.categories.Category; +import org.labkey.api.security.User; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.categories.External; import org.labkey.test.categories.MacCossLabModules; import org.labkey.test.components.panoramapublic.TargetedMsExperimentWebPart; +import org.labkey.test.pages.LabkeyErrorPage; +import org.labkey.test.util.ApiPermissionsHelper; +import org.labkey.test.util.DataRegionTable; +import org.labkey.test.util.PermissionsHelper; + +import java.util.List; + +import static org.junit.Assert.assertEquals; @Category({External.class, MacCossLabModules.class}) -@BaseWebDriverTest.ClassTimeout(minutes = 5) +@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 Dataset 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 projectName = getProjectName(); - String folderName_1 = "Private Data 1"; - String targetFolder_1 = "Private Data 1 Copy"; - String experimentTitle_1 = "Test for private data message reminder - DATA ONE"; - log("Creating data 1 folder and copying to Panorama Public."); - String shortAccessUrl_1 = setupFolderSubmitAndCopy(projectName, folderName_1, targetFolder_1, experimentTitle_1, SUBMITTER, "One", ADMIN_USER, SKY_FILE_1); - - String folderName_2 = "Private Data 2"; - String targetFolder_2 = "Private Data 2 Copy"; - String experimentTitle_2 = "Test for private data message reminder - DATA TWO"; - log("Creating data 2 folder and copying to Panorama Public."); - String shortAccessUrl_2 = setupFolderSubmitAndCopy(projectName, folderName_2, targetFolder_2, experimentTitle_2, SUBMITTER_2, "Two", ADMIN_2, SKY_FILE_1); - - String folderName_3 = "Private Data 3"; - String targetFolder_3 = "Private Data 3 Copy"; - String experimentTitle_3 = "Test for private data message reminder - DATA THREE"; - log("Creating data 3 folder and copying to Panorama Public."); - String shortAccessUrl_3 = setupFolderSubmitAndCopy(projectName, folderName_3, targetFolder_3, experimentTitle_3, SUBMITTER_3, "Three", ADMIN_3, SKY_FILE_1); +// String panoramaPublicProject = "Panorama Public 2"; + 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(projectName, folderName_3, SUBMITTER_3); + makePublic(panoramaPublicProject, folderInfo_3); + folderInfo_3.setPublic(true); log("Verifying data 1 is private"); - verifyIsPublicColumn(PANORAMA_PUBLIC, experimentTitle_1, false); + verifyIsPublicColumn(panoramaPublicProject, folderInfo_1.getExperimentTitle(), false); log("Verifying data 2 is private"); - verifyIsPublicColumn(PANORAMA_PUBLIC, experimentTitle_2, false); + verifyIsPublicColumn(panoramaPublicProject, folderInfo_2.getExperimentTitle(), false); log("Verifying data 3 is public"); - verifyIsPublicColumn(PANORAMA_PUBLIC, experimentTitle_3, true); - - log("Sending reminders"); - sendReminders(); - - portalHelper.enterAdminMode(); - + verifyIsPublicColumn(panoramaPublicProject, folderInfo_3.getExperimentTitle(), true); - goToProjectFolder(projectName, folderName_1); - portalHelper.clickWebpartMenuItem("Targeted MS Experiment ", true, "Support Messages"); - assertTextPresent("Title: Action Required: Status Update for Your Private Dataset on Panorama Public"); + List dataFolderInfos = List.of(folderInfo_1, folderInfo_2, folderInfo_3); + testSendingReminders(panoramaPublicProject, dataFolderInfos); + } - goToProjectFolder(projectName, folderName_2); - portalHelper.clickWebpartMenuItem("Targeted MS Experiment ", true, "Support Messages"); - assertTextPresent("Title: Action Required: Status Update for Your Private Dataset on Panorama Public"); + 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); +// TargetedMsExperimentWebPart expWebPart = new TargetedMsExperimentWebPart(this); +// String shortAccessUrl = expWebPart.getAccessLink(); + goToExperimentDetailsPage(); + int exptAnnotationsId = Integer.parseInt(portalHelper.getUrlParam("id")); + return new DataFolderInfo(sourceFolder, targetFolder, shortAccessUrl, experimentTitle, exptAnnotationsId, submitter); + } - goToProjectFolder(projectName, folderName_3); - portalHelper.clickWebpartMenuItem("Targeted MS Experiment ", true, "Support Messages"); - assertTextNotPresent("Title: Action Required: Status Update for Your Private Dataset on Panorama Public"); + private void gotoSupportMessage(DataFolderInfo folderInfo) + { + portalHelper.clickWebpartMenuItem("Targeted MS Experiment", true, "Support Messages"); + waitForText("Submitted - " + folderInfo.getShortUrl()); } - private void makePublic(String projectName, String folderName, String user) + private void makePublic(String projectName, DataFolderInfo folderInfo) { if (isImpersonating()) { stopImpersonating(true); } - goToProjectFolder(projectName, folderName); - impersonate(user); + goToProjectFolder(projectName, folderInfo.getTargetFolder()); + impersonate(folderInfo.getSubmitter()); goToDashboard(); makeDataPublic(true); stopImpersonating(); } - protected void sendReminders() + 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", "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 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 + +// // Post reminders again. Since the reminder frequency is set to 0, reminders will get posted again. Select all experiments. +// postReminders(false, privateDataCount, -1, ++pipelineJobCount); +// verifyReminderPosted(projectName, privateData.get(0), 2); +// verifyReminderPosted(projectName, privateData.get(0), 1); + + // Change the reminder frequency to 1. + log("Changing reminder settings. Setting reminder frequency to 1."); + saveSettings("2", "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); + String 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 experiments "); + + // Change reminder frequency to 0 again. + log("Changing reminder settings. Setting reminder frequency to 0."); + saveSettings("2", "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 experiments "); + + // 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. + String 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); + + impersonate(SUBMITTER_3); + click(Locator.linkWithText("Request Extension")); + new LabkeyErrorPage(getDriver()).assertUnauthorized(checker()); + + stopImpersonating(); + + goToProjectFolder(projectName, folderInfo.getTargetFolder()); + gotoSupportMessage(folderInfo); + impersonate(folderInfo.getSubmitter()); + + assertTextPresent("Request Extension"); + click(Locator.linkWithText("Request Extension")); + 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"); + 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); + + impersonate(SUBMITTER_3); + click(Locator.linkWithText("Request Deletion")); + new LabkeyErrorPage(getDriver()).assertUnauthorized(checker()); + + stopImpersonating(); + + goToProjectFolder(projectName, folderInfo.getTargetFolder()); + gotoSupportMessage(folderInfo); + impersonate(folderInfo.getSubmitter()); + + assertTextPresent("Request Deletion"); + click(Locator.linkWithText("Request Deletion")); + waitForText("Request Deletion For Panorama Public Data"); + assertTextPresent("You are requesting deletion of the private data on Panorama Public at " + folderInfo.getShortUrl()); + clickButton("OK"); + 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 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, List folderInfos, int count) + { + for (DataFolderInfo folderInfo: folderInfos) + { + verifyReminderPosted(projectName, folderInfo, count); + } + } + 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 reminderFrequency) { goToAdminConsole().goToSettingsSection(); clickAndWait(Locator.linkWithText("Panorama Public")); clickAndWait(Locator.linkWithText("Private Data Reminder Settings")); - // checkCheckbox(Locator.input("testMode")); - setFormElement(Locator.input("extensionLength"), "2"); - setFormElement(Locator.input("reminderFrequency"), "0"); + + setFormElement(Locator.input("extensionLength"), extensionLength); + setFormElement(Locator.input("reminderFrequency"), reminderFrequency); clickButton("Save"); waitForText("Private data message settings saved"); clickAndWait(Locator.linkWithText("Back to Panorama Public Admin Console")); clickAndWait(Locator.linkWithText("Private Data Reminder Settings")); - doAndWaitForPageToLoad(() -> + assertEquals(String.valueOf(extensionLength), getFormElement(Locator.input("extensionLength"))); + assertEquals(String.valueOf(reminderFrequency), getFormElement(Locator.input("reminderFrequency"))); + } + + 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("ExperimentAnnotationsTable", 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"); + + if (selectRowCount == 0) + { + assertTextPresent("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); + click(Locator.linkWithText("Send Reminders Now")); + waitForText("Send Private Data Reminders"); + assertEquals("/" + projectName, getCurrentContainerPath()); + } + + 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) { - clickButton("Send Reminders Now"); - assertAlertContains("Are you sure you want to send reminder messages for private datasets?"); - dismissAllAlerts(); - }); + _sourceFolder = sourceFolder; + _targetFolder = targetFolder; + _shortUrl = shortUrl; + _experimentTitle = experimentTitle; + _experimentAnnotationsId = experimentAnnotationsId; + _submitter = submitter; + } + + public String getSourceFolder() + { + return _sourceFolder; + } - waitForPipelineJobsToComplete(1, "Post private data reminder messages", false); + 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; + } } } From 83729f05e2e80f3268d5c287e272a80de0e7a4b3 Mon Sep 17 00:00:00 2001 From: vagisha Date: Wed, 13 Aug 2025 12:59:45 -0700 Subject: [PATCH 06/18] Renamed PrivateDataMessageSettings -> PrivateDataReminderSettings.java. --- .../panoramapublic/PanoramaPublicController.java | 12 ++++++------ ...ettings.java => PrivateDataReminderSettings.java} | 8 ++++---- .../labkey/panoramapublic/model/DatasetStatus.java | 10 +++++----- .../pipeline/PrivateDataReminderJob.java | 8 ++++---- .../view/privateDataRemindersSettingsForm.jsp | 9 +++++---- 5 files changed, 24 insertions(+), 23 deletions(-) rename panoramapublic/src/org/labkey/panoramapublic/message/{PrivateDataMessageSettings.java => PrivateDataReminderSettings.java} (92%) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 7650d19f..4e473949 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -154,7 +154,7 @@ import org.labkey.panoramapublic.datacite.Doi; import org.labkey.panoramapublic.datacite.DoiMetadata; import org.labkey.panoramapublic.message.PrivateDataMessageScheduler; -import org.labkey.panoramapublic.message.PrivateDataMessageSettings; +import org.labkey.panoramapublic.message.PrivateDataReminderSettings; import org.labkey.panoramapublic.model.CatalogEntry; import org.labkey.panoramapublic.model.DataLicense; import org.labkey.panoramapublic.model.DatasetStatus; @@ -10170,7 +10170,7 @@ public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException erro @Override protected void doValidationForAction(Errors errors) { - PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); if (_datasetStatus != null) { if (_datasetStatus.isExtensionCurrent(settings)) @@ -10205,7 +10205,7 @@ protected void postNotification() public ModelAndView getSuccessView(ShortUrlForm shortUrlForm) { setTitle("Extension Request Success"); - PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + 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 " + _datasetStatus.extensionValidUntilFormatted(settings)), BR(), @@ -10332,7 +10332,7 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow { if (!reshow) { - PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); form.setEnabled(settings.isEnableReminders()); form.setExtensionLength(settings.getExtensionLength()); form.setReminderFrequency(settings.getReminderFrequency()); @@ -10348,11 +10348,11 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow @Override public boolean handlePost(PrivateDataReminderSettingsForm form, BindException errors) throws Exception { - PrivateDataMessageSettings settings = new PrivateDataMessageSettings(); + PrivateDataReminderSettings settings = new PrivateDataReminderSettings(); settings.setEnableReminders(form.isEnabled()); settings.setExtensionLength(form.getExtensionLength()); settings.setReminderFrequency(form.getReminderFrequency()); - PrivateDataMessageSettings.save(settings); + PrivateDataReminderSettings.save(settings); PrivateDataMessageScheduler.getInstance().initialize(settings.isEnableReminders()); return true; diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java similarity index 92% rename from panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java rename to panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java index 3d9a2877..470f5fdd 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageSettings.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java @@ -2,7 +2,7 @@ import org.labkey.api.data.PropertyManager; -public class PrivateDataMessageSettings +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"; @@ -18,11 +18,11 @@ public class PrivateDataMessageSettings private int _extensionLength; private int _reminderFrequency; - public static PrivateDataMessageSettings get() + public static PrivateDataReminderSettings get() { PropertyManager.WritablePropertyMap settingsMap = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, false); - PrivateDataMessageSettings settings = new PrivateDataMessageSettings(); + PrivateDataReminderSettings settings = new PrivateDataReminderSettings(); if(settingsMap != null) { boolean enableReminders = settingsMap.get(PROP_EXTENSION_MONTHS) == null ? DEFAULT_ENABLE_REMINDERS : Boolean.valueOf(settingsMap.get(PROP_ENABLE_REMINDER)); @@ -42,7 +42,7 @@ public static PrivateDataMessageSettings get() return settings; } - public static void save(PrivateDataMessageSettings settings) + 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())); diff --git a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java index 785f31cd..28bec3d7 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -3,7 +3,7 @@ import org.jetbrains.annotations.Nullable; import org.labkey.api.util.DateUtil; import org.labkey.api.view.ShortURLRecord; -import org.labkey.panoramapublic.message.PrivateDataMessageSettings; +import org.labkey.panoramapublic.message.PrivateDataReminderSettings; import java.time.ZoneId; import java.util.Date; @@ -78,7 +78,7 @@ public boolean reminderSent() return _lastReminderDate != null; } - public boolean isExtensionCurrent(PrivateDataMessageSettings settings) + public boolean isExtensionCurrent(PrivateDataReminderSettings settings) { if (_extensionRequestedDate == null) { @@ -94,7 +94,7 @@ public boolean isExtensionCurrent(PrivateDataMessageSettings settings) return extensionDate.isAfter(extensionValidStartDate); } - public boolean isLastReminderRecent(PrivateDataMessageSettings settings) + public boolean isLastReminderRecent(PrivateDataReminderSettings settings) { if (_lastReminderDate == null) { @@ -110,7 +110,7 @@ public boolean isLastReminderRecent(PrivateDataMessageSettings settings) return reminderDate.isAfter(reminderValidStartDate); } - public @Nullable Date extensionValidUntil(PrivateDataMessageSettings settings) + public @Nullable Date extensionValidUntil(PrivateDataReminderSettings settings) { if (_extensionRequestedDate == null) { @@ -125,7 +125,7 @@ public boolean isLastReminderRecent(PrivateDataMessageSettings settings) ); } - public @Nullable String extensionValidUntilFormatted(PrivateDataMessageSettings settings) + public @Nullable String extensionValidUntilFormatted(PrivateDataReminderSettings settings) { return format(extensionValidUntil(settings)); } diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index b510b9f5..c245a17a 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -17,7 +17,7 @@ import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.panoramapublic.PanoramaPublicManager; import org.labkey.panoramapublic.PanoramaPublicNotification; -import org.labkey.panoramapublic.message.PrivateDataMessageSettings; +import org.labkey.panoramapublic.message.PrivateDataReminderSettings; import org.labkey.panoramapublic.model.DatasetStatus; import org.labkey.panoramapublic.model.ExperimentAnnotations; import org.labkey.panoramapublic.model.Journal; @@ -71,7 +71,7 @@ public static List getPrivateDatasets(Journal panoramaPublic) Set subFolders = ContainerManager.getAllChildren(panoramaPublic.getProject()); List privateDataIds = new ArrayList<>(); - PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); for (Container folder : subFolders) { ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder); @@ -81,7 +81,7 @@ public static List getPrivateDatasets(Journal panoramaPublic) return privateDataIds; } - private static ReminderDecision getReminderDecision(ExperimentAnnotations exptAnnotations, PrivateDataMessageSettings settings) + private static ReminderDecision getReminderDecision(ExperimentAnnotations exptAnnotations, PrivateDataReminderSettings settings) { if (exptAnnotations == null) return ReminderDecision.skip("Experiment annotations are null"); @@ -157,7 +157,7 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) int done = 0; - PrivateDataMessageSettings settings = PrivateDataMessageSettings.get(); + PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); AnnouncementService announcementSvc = AnnouncementService.get(); diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp index f3624680..c1aecac2 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp @@ -20,7 +20,7 @@ <%@ page import="org.labkey.api.view.JspView" %> <%@ page import="org.labkey.panoramapublic.PanoramaPublicController" %> <%@ page import="org.labkey.api.view.template.ClientDependencies" %> -<%@ page import="org.labkey.panoramapublic.message.PrivateDataMessageSettings" %> +<%@ 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" %> @@ -28,6 +28,7 @@ <%@ 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" %> <%! @@ -74,7 +75,7 @@ @@ -97,6 +96,14 @@ + + + + @@ -93,6 +98,11 @@ @@ -101,6 +111,9 @@ + + + + diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index df358543..11b29547 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -299,10 +299,10 @@ private void saveSettings(String extensionLength, String delayUntilFirstReminder setFormElement(Locator.input("reminderFrequency"), reminderFrequency); setFormElement(Locator.input("extensionLength"), extensionLength); clickButton("Save", 0); - waitForText("Private data message settings saved"); - clickAndWait(Locator.linkWithText("Back to Panorama Public Admin Console")); + waitForText("Private data reminder settings saved"); + clickAndWait(Locator.linkWithText("Back to Private Data Reminder Settings")); - clickAndWait(Locator.linkWithText("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"))); From f13c7ed8f09441b65b5f45813f28e6bfb474c6f3 Mon Sep 17 00:00:00 2001 From: vagisha Date: Thu, 28 Aug 2025 14:12:22 -0700 Subject: [PATCH 16/18] - Replaced "Home" button with "Data Folder" link in the success view of RequestDeletionAction. - Fixed SQLException by updating DatasetStatus handling: - Delete row in DatasetStatus when data is resubmitted and a new Panorama Public copy is created. - Delete row in DatasetStatus when the Panorama Public copy is deleted. - Prevents SQLException from shortUrl FK constraint when deleting Panorama Public copy followed by source data folder, or vice versa. - Cleanup user accounts in PrivateDataReminderTest. --- .../panoramapublic/PanoramaPublicController.java | 4 +++- .../pipeline/CopyExperimentFinalTask.java | 7 +++++++ .../panoramapublic/query/DatasetStatusManager.java | 14 ++++++++++++++ .../query/ExperimentAnnotationsManager.java | 4 ++++ .../panoramapublic/query/JournalManager.java | 1 + .../panoramapublic/PrivateDataReminderTest.java | 9 +++++++++ 6 files changed, 38 insertions(+), 1 deletion(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 39f85fc8..cc5c3f62 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10548,7 +10548,9 @@ 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(new ButtonBuilder("Home").submit(false).href(AppProps.getInstance().getHomePageActionURL())) + DIV( + LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer())) + ) )); } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java index cb2c0138..a27d10bc 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; @@ -150,6 +151,12 @@ private void finishUp(PipelineJob job, CopyExperimentJobSupport jobSupport) thro // Update the row in panoramapublic.ExperimentAnnotations - set the shortURL and version ExperimentAnnotations targetExperiment = updateExperimentAnnotations(container, sourceExperiment, js, user, log); + if (previousCopy != null) + { + // If this is a re-copy, a row may exist in DatasetStatus for the short URL associated with the experiment. Remove it now. + // This is a fresh copy, so data status should be reset. + DatasetStatusManager.deleteStatusForExperiment(targetExperiment); + } // If there is a Panorama Public data catalog entry associated with the previous copy of the experiment, move it to the // new container. diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java index 8c15091e..515888fa 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java @@ -1,6 +1,7 @@ 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; @@ -9,6 +10,7 @@ import org.labkey.api.view.ShortURLRecord; import org.labkey.panoramapublic.PanoramaPublicManager; import org.labkey.panoramapublic.model.DatasetStatus; +import org.labkey.panoramapublic.model.ExperimentAnnotations; public class DatasetStatusManager { @@ -37,4 +39,16 @@ public static void update(DatasetStatus datasetStatus, User user) { Table.update(user, PanoramaPublicManager.getTableInfoDatasetStatus(), datasetStatus, datasetStatus.getId()); } + + public static void deleteStatusForExperiment(ExperimentAnnotations expAnnotations) + { + DatasetStatus status = getForShortUrl(expAnnotations.getShortUrl()); + if (status == null) return; + + try(DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction()) + { + Table.delete(PanoramaPublicManager.getTableInfoDatasetStatus(), new SimpleFilter(FieldKey.fromParts("id"), status.getId())); + transaction.commit(); + } + } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java index 230369b6..2c03d8a7 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java @@ -283,6 +283,10 @@ private static void deleteExperiment(ExperimentAnnotations expAnnotations, User } else { + // Delete the row in DatasetStatus for this experiment. Do this before setting the experiment's shortURL to null + // in SubmissionManager.beforeCopiedExperimentDeleted(). + DatasetStatusManager.deleteStatusForExperiment(expAnnotations); + // This experiment is a journal copy (i.e. in the Panorama Public project on PanoramaWeb) SubmissionManager.beforeCopiedExperimentDeleted(expAnnotations, user); } diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java index 77dc5048..05b48cd5 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java @@ -162,6 +162,7 @@ public static void delete(Journal journal, User user) List expAnnotations = getExperimentsForJournal(journal.getId()); for(ExperimentAnnotations expAnnotation: expAnnotations) { + DatasetStatusManager.deleteStatusForExperiment(expAnnotation); removeJournalAccess(expAnnotation, journal, user); } SubmissionManager.deleteAllSubmissionsForJournal(journal.getId()); diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index 11b29547..3619b747 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -4,6 +4,7 @@ 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; @@ -381,6 +382,14 @@ private void goToSendRemindersPage(String projectName) 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; From deb4dc1bc82d6c3bc93c8a173c93d572d1b582ee Mon Sep 17 00:00:00 2001 From: vagisha Date: Fri, 29 Aug 2025 08:08:54 -0700 Subject: [PATCH 17/18] - Replaced ShortUrl column in DatasetStatus table with ExperimentAnnotationsId. DatasetStatus is associated with a particular copy of the data. If a new copy is made, the data status resets. Status associated with the previous copy is no longer relevant. --- .../panoramapublic-25.002-25.003.sql | 32 +++++++++++++++++++ .../resources/schemas/panoramapublic.xml | 8 +---- .../PanoramaPublicController.java | 10 +++--- .../panoramapublic/PanoramaPublicModule.java | 2 +- .../panoramapublic/model/DatasetStatus.java | 10 +++--- .../pipeline/CopyExperimentFinalTask.java | 6 ---- .../pipeline/PrivateDataReminderJob.java | 6 ++-- .../query/DatasetStatusManager.java | 12 +++---- .../query/DatasetStatusTableInfo.java | 17 +++------- .../query/ExperimentAnnotationsManager.java | 7 ++-- .../query/ExperimentAnnotationsTableInfo.java | 16 +++++----- .../panoramapublic/query/JournalManager.java | 1 - 12 files changed, 68 insertions(+), 59 deletions(-) create mode 100644 panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.002-25.003.sql diff --git a/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.002-25.003.sql b/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.002-25.003.sql new file mode 100644 index 00000000..81bf665f --- /dev/null +++ b/panoramapublic/resources/schemas/dbscripts/postgresql/panoramapublic-25.002-25.003.sql @@ -0,0 +1,32 @@ +-- ------------------------------------------------------------------------ +-- Replace the ShortUrl column with an ExperimentAnnotationsId column. +-- ------------------------------------------------------------------------ + +-- Add ExperimentAnnotationsId column as nullable first +ALTER TABLE panoramapublic.DatasetStatus ADD COLUMN ExperimentAnnotationsId INT; + +-- Populate the column by matching ShortUrl values +UPDATE panoramapublic.DatasetStatus +SET ExperimentAnnotationsId = ea.Id +FROM panoramapublic.ExperimentAnnotations ea +WHERE ea.ShortUrl = panoramapublic.DatasetStatus.ShortUrl; + +-- Delete rows that couldn't be matched (where ExperimentAnnotationsId is still null) +DELETE FROM panoramapublic.DatasetStatus WHERE ExperimentAnnotationsId IS NULL; + +-- Now make ExperimentAnnotationsId NOT NULL +ALTER TABLE panoramapublic.DatasetStatus ALTER COLUMN ExperimentAnnotationsId SET NOT NULL; + +-- Add constraints and index +ALTER TABLE panoramapublic.DatasetStatus ADD CONSTRAINT FK_DatasetStatus_ExperimentAnnotations FOREIGN KEY (ExperimentAnnotationsId) REFERENCES panoramapublic.ExperimentAnnotations(Id); +ALTER TABLE panoramapublic.DatasetStatus ADD CONSTRAINT UQ_DatasetStatus_ExperimentAnnotations UNIQUE (ExperimentAnnotationsId); +CREATE INDEX IX_DatasetStatus_ExperimentAnnotations ON panoramapublic.DatasetStatus(ExperimentAnnotationsId); + +-- Drop old constraints and index +ALTER TABLE panoramapublic.DatasetStatus DROP CONSTRAINT FK_DatasetStatus_ShortUrl; +ALTER TABLE panoramapublic.DatasetStatus DROP CONSTRAINT UQ_DatasetStatus_ShortUrl; +DROP INDEX panoramapublic.IX_DatasetStatus_ShortUrl; + +-- Finally drop the ShortUrl column +ALTER TABLE panoramapublic.DatasetStatus DROP COLUMN ShortUrl; + diff --git a/panoramapublic/resources/schemas/panoramapublic.xml b/panoramapublic/resources/schemas/panoramapublic.xml index 95be5aaf..6b3acf79 100644 --- a/panoramapublic/resources/schemas/panoramapublic.xml +++ b/panoramapublic/resources/schemas/panoramapublic.xml @@ -860,13 +860,7 @@ true - - - EntityId - core - ShortUrl - - + diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index cc5c3f62..fe84ac06 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10396,7 +10396,7 @@ public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) ensureCorrectContainer(getContainer(), _exptAnnotations.getContainer(), getViewContext()); - _datasetStatus = DatasetStatusManager.getForShortUrl(_exptAnnotations.getShortUrl()); + _datasetStatus = DatasetStatusManager.getForExperiment(_exptAnnotations); // Action-specific validation doValidationForAction(errors); @@ -10410,7 +10410,7 @@ public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throw if (_datasetStatus == null) { _datasetStatus = new DatasetStatus(); - _datasetStatus.setShortUrl(_exptAnnotations.getShortUrl()); + _datasetStatus.setExperimentAnnotationsId(_exptAnnotations.getId()); updateDatasetStatus(_datasetStatus); DatasetStatusManager.save(_datasetStatus, getUser()); } @@ -10458,13 +10458,13 @@ protected void doValidationForAction(Errors errors) PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); if (settings.isExtensionValid(_datasetStatus)) { - errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + _datasetStatus.getShortUrl().renderShortURL() + 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 " + _datasetStatus.getShortUrl().renderShortURL()); + + " for the data with short URL " + _exptAnnotations.getShortUrl().renderShortURL()); } } } @@ -10522,7 +10522,7 @@ protected void doValidationForAction(Errors errors) if (_datasetStatus.deletionRequested()) { errors.reject(ERROR_MSG, "A deletion request was already submitted on " + _datasetStatus.getDeletionRequestedDateFormatted() - + " for the data with short URL " + _datasetStatus.getShortUrl().renderShortURL()); + + " for the data with short URL " + _exptAnnotations.getShortUrl().renderShortURL()); } } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java index e253c158..6e4d2c6c 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java @@ -92,7 +92,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 25.002; + return 25.003; } @Override diff --git a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java index ce7c2177..717da27d 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -8,19 +8,19 @@ public class DatasetStatus extends DbEntity { - private ShortURLRecord _shortUrl; + private int _experimentAnnotationsId; private Date _lastReminderDate; private Date _extensionRequestedDate; private Date _deletionRequestedDate; - public ShortURLRecord getShortUrl() + public int getExperimentAnnotationsId() { - return _shortUrl; + return _experimentAnnotationsId; } - public void setShortUrl(ShortURLRecord shortUrl) + public void setExperimentAnnotationsId(int experimentAnnotationsId) { - _shortUrl = shortUrl; + _experimentAnnotationsId = experimentAnnotationsId; } public Date getLastReminderDate() diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java index a27d10bc..362f450e 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java @@ -151,12 +151,6 @@ private void finishUp(PipelineJob job, CopyExperimentJobSupport jobSupport) thro // Update the row in panoramapublic.ExperimentAnnotations - set the shortURL and version ExperimentAnnotations targetExperiment = updateExperimentAnnotations(container, sourceExperiment, js, user, log); - if (previousCopy != null) - { - // If this is a re-copy, a row may exist in DatasetStatus for the short URL associated with the experiment. Remove it now. - // This is a fresh copy, so data status should be reset. - DatasetStatusManager.deleteStatusForExperiment(targetExperiment); - } // If there is a Panorama Public data catalog entry associated with the previous copy of the experiment, move it to the // new container. diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index ed2ce76f..2b5bfe6b 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -100,7 +100,7 @@ private static ReminderDecision getReminderDecision(@NotNull ExperimentAnnotatio return ReminderDecision.skip("Not the current version of the experiment"); } - DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(exptAnnotations.getShortUrl()); + DatasetStatus datasetStatus = DatasetStatusManager.getForExperiment(exptAnnotations); if (datasetStatus != null) { if (datasetStatus.deletionRequested()) @@ -293,11 +293,11 @@ private void postReminderMessage(ExperimentAnnotations expAnnotations, JournalSu private void updateDatasetStatus(ExperimentAnnotations expAnnotations) { - DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(expAnnotations.getShortUrl()); + DatasetStatus datasetStatus = DatasetStatusManager.getForExperiment(expAnnotations); if (datasetStatus == null) { datasetStatus = new DatasetStatus(); - datasetStatus.setShortUrl(expAnnotations.getShortUrl()); + datasetStatus.setExperimentAnnotationsId(expAnnotations.getId()); datasetStatus.setLastReminderDate(new Date()); DatasetStatusManager.save(datasetStatus, getUser()); } diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java index 515888fa..c1c2ca5c 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusManager.java @@ -7,7 +7,6 @@ import org.labkey.api.data.TableSelector; import org.labkey.api.query.FieldKey; import org.labkey.api.security.User; -import org.labkey.api.view.ShortURLRecord; import org.labkey.panoramapublic.PanoramaPublicManager; import org.labkey.panoramapublic.model.DatasetStatus; import org.labkey.panoramapublic.model.ExperimentAnnotations; @@ -19,12 +18,12 @@ public static DatasetStatus get(int datasetStatusId) return new TableSelector(PanoramaPublicManager.getTableInfoDatasetStatus(),null, null).getObject(datasetStatusId, DatasetStatus.class); } - public static @Nullable DatasetStatus getForShortUrl(ShortURLRecord shortUrl) + public static @Nullable DatasetStatus getForExperiment(ExperimentAnnotations experimentAnnotations) { - if (shortUrl != null) + if (experimentAnnotations != null) { SimpleFilter filter = new SimpleFilter(); - filter.addCondition(FieldKey.fromParts("ShortUrl"), shortUrl.getEntityId()); + filter.addCondition(FieldKey.fromParts("ExperimentAnnotationsId"), experimentAnnotations.getId()); return new TableSelector(PanoramaPublicManager.getTableInfoDatasetStatus(), filter, null).getObject(DatasetStatus.class); } return null; @@ -42,12 +41,11 @@ public static void update(DatasetStatus datasetStatus, User user) public static void deleteStatusForExperiment(ExperimentAnnotations expAnnotations) { - DatasetStatus status = getForShortUrl(expAnnotations.getShortUrl()); - if (status == null) return; + if (expAnnotations == null) return; try(DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction()) { - Table.delete(PanoramaPublicManager.getTableInfoDatasetStatus(), new SimpleFilter(FieldKey.fromParts("id"), status.getId())); + 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 index c36b1d70..afeb6a52 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java @@ -19,20 +19,13 @@ public class DatasetStatusTableInfo extends PanoramaPublicTable public DatasetStatusTableInfo(@NotNull PanoramaPublicSchema userSchema, ContainerFilter cf) { super(PanoramaPublicManager.getTableInfoDatasetStatus(), userSchema, cf, - new ContainerJoin("ShortUrl", PanoramaPublicManager.getTableInfoExperimentAnnotations(), "ShortUrl")); + new ContainerJoin("ExperimentAnnotationsId", PanoramaPublicManager.getTableInfoExperimentAnnotations(), "Id")); + var shortUrlCol = wrapColumn("ShortURL", getRealTable().getColumn("ExperimentAnnotationsId")); + shortUrlCol.setDisplayColumnFactory(new ShortUrlDisplayColumnFactory(FieldKey.fromParts("ShortUrl"))); + addColumn(shortUrlCol); - var accessUrlCol = getMutableColumn(FieldKey.fromParts("ShortUrl")); - if (accessUrlCol != null) - { - accessUrlCol.setDisplayColumnFactory(new ShortUrlDisplayColumnFactory()); - } - - SQLFragment expColSql = new SQLFragment(" (SELECT Id FROM ") - .append(PanoramaPublicManager.getTableInfoExperimentAnnotations(), "exp") - .append(" WHERE exp.shortUrl = ").append(ExprColumn.STR_TABLE_ALIAS).append(".shortUrl") - .append(") "); - var experimentTitleCol = new ExprColumn(this, "Title", expColSql, JdbcType.VARCHAR); + var experimentTitleCol = wrapColumn("Title", getRealTable().getColumn(FieldKey.fromParts("ExperimentAnnotationsId"))); experimentTitleCol.setFk(QueryForeignKey.from(getUserSchema(), cf).schema(getUserSchema()).to(PanoramaPublicSchema.TABLE_EXPERIMENT_ANNOTATIONS, "Id", null)); addColumn(experimentTitleCol); diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java index 2c03d8a7..b23fbe90 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsManager.java @@ -283,10 +283,6 @@ private static void deleteExperiment(ExperimentAnnotations expAnnotations, User } else { - // Delete the row in DatasetStatus for this experiment. Do this before setting the experiment's shortURL to null - // in SubmissionManager.beforeCopiedExperimentDeleted(). - DatasetStatusManager.deleteStatusForExperiment(expAnnotations); - // This experiment is a journal copy (i.e. in the Panorama Public project on PanoramaWeb) SubmissionManager.beforeCopiedExperimentDeleted(expAnnotations, user); } @@ -305,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 f9bbfd92..bf530acc 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java @@ -411,18 +411,18 @@ private ExprColumn getCatalogEntryCol() private ExprColumn getDatasetStatusCol(ContainerFilter cf) { - SQLFragment datasetStatusSql = new SQLFragment(" (SELECT status.shorturl AS DatasetStatus ") + SQLFragment datasetStatusSql = new SQLFragment(" (SELECT status.Id AS DatasetStatus ") .append(" FROM ").append(PanoramaPublicManager.getTableInfoDatasetStatus(), "status") .append(" WHERE ") - .append(" status.shortUrl = ").append(ExprColumn.STR_TABLE_ALIAS).append(".shortUrl") + .append(" status.experimentAnnotationsId = ").append(ExprColumn.STR_TABLE_ALIAS).append(".Id") .append(") "); - ExprColumn col = new ExprColumn(this, "DatasetStatus", datasetStatusSql, JdbcType.VARCHAR); + 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, "shortUrl", null)); + .to(PanoramaPublicSchema.TABLE_DATASET_STATUS, "Id", null)); return col; } @@ -876,7 +876,7 @@ public void renderGridCellContents(RenderContext ctx, HtmlWriter out) public static class DatasetStatusColumn extends DataColumn { - private final FieldKey ID_COL = new FieldKey(getColumnInfo().getFieldKey(), "id"); + private final FieldKey EXPT_ANNOTATIONS_ID_COL = new FieldKey(getColumnInfo().getFieldKey(), "experimentAnnotationsId"); public DatasetStatusColumn(ColumnInfo col) { @@ -892,10 +892,10 @@ public Object getDisplayValue(RenderContext ctx) { return ""; } - Integer statusId = ctx.get(ID_COL, Integer.class); + Integer statusId = ctx.get(getColumnInfo().getFieldKey(), Integer.class); // Get the experiment connected with this status Id. - Integer experimentId = ctx.get(FieldKey.fromParts("id"), Integer.class); + Integer experimentId = ctx.get(EXPT_ANNOTATIONS_ID_COL, Integer.class); if (experimentId != null) { ExperimentAnnotations expAnnot = ExperimentAnnotationsManager.get(experimentId); @@ -925,7 +925,7 @@ public Object getDisplayValue(RenderContext ctx) public void addQueryFieldKeys(Set keys) { super.addQueryFieldKeys(keys); - keys.add(ID_COL); + keys.add(EXPT_ANNOTATIONS_ID_COL); } @Override diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java index 05b48cd5..77dc5048 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java @@ -162,7 +162,6 @@ public static void delete(Journal journal, User user) List expAnnotations = getExperimentsForJournal(journal.getId()); for(ExperimentAnnotations expAnnotation: expAnnotations) { - DatasetStatusManager.deleteStatusForExperiment(expAnnotation); removeJournalAccess(expAnnotation, journal, user); } SubmissionManager.deleteAllSubmissionsForJournal(journal.getId()); From d435f02d7dc360eadb16556ad90437aedc7f9b6a Mon Sep 17 00:00:00 2001 From: vagisha Date: Tue, 9 Sep 2025 11:13:41 -0700 Subject: [PATCH 18/18] Simplify adding Short URL and experiment title columns to DatasetStatusTableInfo. --- .../panoramapublic/query/DatasetStatusTableInfo.java | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java index afeb6a52..e0413edd 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java @@ -21,21 +21,13 @@ public DatasetStatusTableInfo(@NotNull PanoramaPublicSchema userSchema, Containe super(PanoramaPublicManager.getTableInfoDatasetStatus(), userSchema, cf, new ContainerJoin("ExperimentAnnotationsId", PanoramaPublicManager.getTableInfoExperimentAnnotations(), "Id")); - var shortUrlCol = wrapColumn("ShortURL", getRealTable().getColumn("ExperimentAnnotationsId")); - shortUrlCol.setDisplayColumnFactory(new ShortUrlDisplayColumnFactory(FieldKey.fromParts("ShortUrl"))); - addColumn(shortUrlCol); - - var experimentTitleCol = wrapColumn("Title", getRealTable().getColumn(FieldKey.fromParts("ExperimentAnnotationsId"))); - experimentTitleCol.setFk(QueryForeignKey.from(getUserSchema(), cf).schema(getUserSchema()).to(PanoramaPublicSchema.TABLE_EXPERIMENT_ANNOTATIONS, "Id", null)); - addColumn(experimentTitleCol); - 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("ShortUrl")); - visibleColumns.add(FieldKey.fromParts("Title")); + 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"));
- <%=h(PrivateDataMessageSettings.PROP_ENABLE_REMINDER)%> + <%=h(PrivateDataReminderSettings.PROP_ENABLE_REMINDER)%> /> @@ -82,7 +83,7 @@
- <%=h(PrivateDataMessageSettings.PROP_EXTENSION_MONTHS)%> + <%=h(PrivateDataReminderSettings.PROP_EXTENSION_MONTHS)%> @@ -90,7 +91,7 @@
- <%=h(PrivateDataMessageSettings.PROP_REMINDER_FREQUENCY)%> + <%=h(PrivateDataReminderSettings.PROP_REMINDER_FREQUENCY)%> From 1efdd74d6de2a8df464a14a487a6e683545aaf8b Mon Sep 17 00:00:00 2001 From: vagisha Date: Wed, 13 Aug 2025 15:59:20 -0700 Subject: [PATCH 07/18] Escape tilde (~). In the LabKey Markdown flavor, text between single tildes (e.g., ~strikethrough~) is rendered as strikethrough. --- .../PanoramaPublicNotification.java | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java index 4314ad35..f16df630 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -10,6 +10,7 @@ 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; @@ -592,9 +593,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_+" will result 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, @@ -715,8 +720,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))); } } } From be1d85e6fe2dd7ba59972ec32dedee926086516b Mon Sep 17 00:00:00 2001 From: vagisha Date: Thu, 14 Aug 2025 00:16:21 -0700 Subject: [PATCH 08/18] Added DelayUntilFirstReminder property --- .../PanoramaPublicController.java | 17 ++++++- .../PanoramaPublicNotification.java | 4 +- .../message/PrivateDataReminderSettings.java | 33 +++++++++--- .../panoramapublic/model/DatasetStatus.java | 6 +-- .../pipeline/PrivateDataReminderJob.java | 51 ++++++++++++++----- .../view/privateDataRemindersSettingsForm.jsp | 13 +++-- .../PrivateDataReminderTest.java | 42 +++++++++------ 7 files changed, 120 insertions(+), 46 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 4e473949..dd4f4540 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10334,8 +10334,9 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow { PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); form.setEnabled(settings.isEnableReminders()); - form.setExtensionLength(settings.getExtensionLength()); + form.setDelayUntilFirstReminder(settings.getDelayUntilFirstReminder()); form.setReminderFrequency(settings.getReminderFrequency()); + form.setExtensionLength(settings.getExtensionLength()); } VBox view = new VBox(); @@ -10350,8 +10351,9 @@ public boolean handlePost(PrivateDataReminderSettingsForm form, BindException er { PrivateDataReminderSettings settings = new PrivateDataReminderSettings(); settings.setEnableReminders(form.isEnabled()); - settings.setExtensionLength(form.getExtensionLength()); + settings.setDelayUntilFirstReminder(form.getDelayUntilFirstReminder()); settings.setReminderFrequency(form.getReminderFrequency()); + settings.setExtensionLength(form.getExtensionLength()); PrivateDataReminderSettings.save(settings); PrivateDataMessageScheduler.getInstance().initialize(settings.isEnableReminders()); @@ -10387,6 +10389,7 @@ public static class PrivateDataReminderSettingsForm private boolean _enabled; private int _extensionLength; private int _reminderFrequency; + private int _delayUntilFirstReminder; public boolean isEnabled() { @@ -10417,6 +10420,16 @@ public void setReminderFrequency(int reminderFrequency) { _reminderFrequency = reminderFrequency; } + + public int getDelayUntilFirstReminder() + { + return _delayUntilFirstReminder; + } + + public void setDelayUntilFirstReminder(int delayUntilFirstReminder) + { + _delayUntilFirstReminder = delayUntilFirstReminder; + } } @RequiresPermission(AdminOperationsPermission.class) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java index f16df630..55bb7f89 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -22,6 +22,7 @@ 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.DatasetStatus; import org.labkey.panoramapublic.model.ExperimentAnnotations; import org.labkey.panoramapublic.model.Journal; @@ -304,6 +305,7 @@ public static void postPrivateStatusExtensionMessage(@NotNull Journal journal, @ { throw new NotFoundException(String.format("Could not find an admin user for %s.", journal.getName())); } + PrivateDataReminderSettings reminderSettings = PrivateDataReminderSettings.get(); /* Thank you for your request to extend the private status of your data on Panorama Public at . Your data has been granted an extension for an additional 6 months. You’ll receive another reminder at that time, or you may make the dataset public earlier. @@ -313,7 +315,7 @@ public static void postPrivateStatusExtensionMessage(@NotNull Journal journal, @ 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 " + DatasetStatus.EXTENSION_VALID_MONTHS + " month extension. ") + .append("Your data has been granted a " + reminderSettings.getExtensionLength() + " month extension. ") .append("You’ll 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: ") diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java index 470f5fdd..5faa6c1f 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java @@ -6,17 +6,22 @@ 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_EXTENSION_MONTHS = "Extension duration (months)"; + 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_MONTHS = "Extension duration (months)"; + private static final boolean DEFAULT_ENABLE_REMINDERS = false; - private static final int DEFAULT_EXTENSION_LENGTH = 6; // Private status of a dataset can be extended by 6 months. + 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. private boolean _enableReminders; - private int _extensionLength; + private int _delayUntilFirstReminder; private int _reminderFrequency; + private int _extensionLength; + public static PrivateDataReminderSettings get() { @@ -27,16 +32,19 @@ public static PrivateDataReminderSettings get() { boolean enableReminders = settingsMap.get(PROP_EXTENSION_MONTHS) == null ? DEFAULT_ENABLE_REMINDERS : Boolean.valueOf(settingsMap.get(PROP_ENABLE_REMINDER)); settings.setEnableReminders(enableReminders); - int extensionLength = settingsMap.get(PROP_EXTENSION_MONTHS) == null ? DEFAULT_EXTENSION_LENGTH : Integer.valueOf(settingsMap.get(PROP_EXTENSION_MONTHS)); - settings.setExtensionLength(extensionLength); + 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_EXTENSION_LENGTH : Integer.valueOf(settingsMap.get(PROP_REMINDER_FREQUENCY)); settings.setReminderFrequency(reminderFrequency); + int extensionLength = settingsMap.get(PROP_EXTENSION_MONTHS) == null ? DEFAULT_EXTENSION_LENGTH : Integer.valueOf(settingsMap.get(PROP_EXTENSION_MONTHS)); + settings.setExtensionLength(extensionLength); } else { settings.setEnableReminders(DEFAULT_ENABLE_REMINDERS); - settings.setExtensionLength(DEFAULT_EXTENSION_LENGTH); + settings.setDelayUntilFirstReminder(DEFAULT_DELAY_UNTIL_FIRST_REMINDER); settings.setReminderFrequency(DEFAULT_REMINDER_FREQUENCY); + settings.setExtensionLength(DEFAULT_EXTENSION_LENGTH); } return settings; @@ -46,8 +54,9 @@ 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_EXTENSION_MONTHS, String.valueOf(settings.getExtensionLength())); + settingsMap.put(PROP_DELAY_UNTIL_FIRST_REMINDER, String.valueOf(settings.getDelayUntilFirstReminder())); settingsMap.put(PROP_REMINDER_FREQUENCY, String.valueOf(settings.getReminderFrequency())); + settingsMap.put(PROP_EXTENSION_MONTHS, String.valueOf(settings.getExtensionLength())); settingsMap.save(); } @@ -80,4 +89,14 @@ public int getReminderFrequency() { return _reminderFrequency; } + + public int getDelayUntilFirstReminder() + { + return _delayUntilFirstReminder; + } + + public void setDelayUntilFirstReminder(int delayUntilFirstReminder) + { + _delayUntilFirstReminder = delayUntilFirstReminder; + } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java index 28bec3d7..0fbace88 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -16,8 +16,6 @@ public class DatasetStatus extends DbEntity private Date _extensionRequestedDate; private Date _deletionRequestedDate; - public static final int EXTENSION_VALID_MONTHS = 6; // 6 months - public ShortURLRecord getShortUrl() { return _shortUrl; @@ -110,7 +108,7 @@ public boolean isLastReminderRecent(PrivateDataReminderSettings settings) return reminderDate.isAfter(reminderValidStartDate); } - public @Nullable Date extensionValidUntil(PrivateDataReminderSettings settings) + public @Nullable Date getExtensionValidUntilDate(PrivateDataReminderSettings settings) { if (_extensionRequestedDate == null) { @@ -127,7 +125,7 @@ public boolean isLastReminderRecent(PrivateDataReminderSettings settings) public @Nullable String extensionValidUntilFormatted(PrivateDataReminderSettings settings) { - return format(extensionValidUntil(settings)); + return format(getExtensionValidUntilDate(settings)); } private @Nullable String format(Date date) diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index c245a17a..e599030b 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -2,6 +2,7 @@ import org.apache.commons.lang3.StringUtils; 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; @@ -11,6 +12,7 @@ import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.portal.ProjectUrls; import org.labkey.api.security.User; +import org.labkey.api.util.DateUtil; import org.labkey.api.util.FileUtil; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.URLHelper; @@ -28,6 +30,9 @@ import org.labkey.panoramapublic.query.SubmissionManager; import java.time.Instant; +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; @@ -81,30 +86,50 @@ public static List getPrivateDatasets(Journal panoramaPublic) return privateDataIds; } - private static ReminderDecision getReminderDecision(ExperimentAnnotations exptAnnotations, PrivateDataReminderSettings settings) + private static ReminderDecision getReminderDecision(@NotNull ExperimentAnnotations exptAnnotations, @NotNull PrivateDataReminderSettings settings) { - if (exptAnnotations == null) - return ReminderDecision.skip("Experiment annotations are null"); - if (exptAnnotations.isPublic()) - return ReminderDecision.skip("Dataset is already public"); + { + return ReminderDecision.skip("Data is already public"); + } if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) + { return ReminderDecision.skip("Not the current version of the experiment"); + } DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(exptAnnotations.getShortUrl()); - if (datasetStatus == null) - return ReminderDecision.post(); + if (datasetStatus != null) + { + if (datasetStatus.deletionRequested()) + { + return ReminderDecision.skip("Submitter has requested deletion"); + } - if (datasetStatus.deletionRequested()) - return ReminderDecision.skip("Submitter has requested deletion"); + if (datasetStatus.isExtensionCurrent(settings)) + { + return ReminderDecision.skip("Submitter requested an extension. Extension is current."); + } - if (datasetStatus.isExtensionCurrent(settings)) - return ReminderDecision.skip("Submitter requested an extension. Extension is current."); + if (datasetStatus.isLastReminderRecent(settings)) + { + return ReminderDecision.skip("Recent reminder already sent"); + } + } + return reminderIsDue(exptAnnotations, settings); + } - if (datasetStatus.isLastReminderRecent(settings)) - return ReminderDecision.skip("Recent reminder already sent"); + 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("MMMM d, yyyy")))); + } return ReminderDecision.post(); } diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp index c1aecac2..4d80d74d 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp @@ -18,7 +18,6 @@ <%@ 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 import="org.labkey.panoramapublic.message.PrivateDataReminderSettings" %> <%@ page import="org.labkey.api.view.ActionURL" %> @@ -83,10 +82,10 @@
- <%=h(PrivateDataReminderSettings.PROP_EXTENSION_MONTHS)%> + <%=h(PrivateDataReminderSettings.PROP_DELAY_UNTIL_FIRST_REMINDER)%> - +
+ <%=h(PrivateDataReminderSettings.PROP_EXTENSION_MONTHS)%> + + +
<%=button("Save").submit(true)%> <%=button("Cancel").href(panoramaPublicAdminUrl)%> diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index c8872cce..c5882c97 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -2,17 +2,16 @@ import org.junit.Test; import org.junit.experimental.categories.Category; -import org.labkey.api.security.User; import org.labkey.test.BaseWebDriverTest; import org.labkey.test.Locator; import org.labkey.test.categories.External; import org.labkey.test.categories.MacCossLabModules; -import org.labkey.test.components.panoramapublic.TargetedMsExperimentWebPart; import org.labkey.test.pages.LabkeyErrorPage; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; -import org.labkey.test.util.PermissionsHelper; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; import java.util.List; import static org.junit.Assert.assertEquals; @@ -133,12 +132,25 @@ private void testSendingReminders(String projectName, List dataF assertEquals(2, privateDataCount); log("Changing reminder settings. Setting reminder frequency to 0."); - saveSettings("2", "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); + var reminderDueDate = LocalDate.now().plusMonths(12).format(DateTimeFormatter.ofPattern("MMMM d, yyyy")); + String message = String.format("Skipping reminder for experiment Id %d - First reminder not due until %s", privateData.get(0).getExperimentAnnotationsId(), reminderDueDate); + String message2 = String.format("Skipping reminder for experiment Id %d - First reminder not due until %s", privateData.get(1).getExperimentAnnotationsId(), reminderDueDate); + 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); @@ -151,24 +163,19 @@ private void testSendingReminders(String projectName, List dataF 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 -// // Post reminders again. Since the reminder frequency is set to 0, reminders will get posted again. Select all experiments. -// postReminders(false, privateDataCount, -1, ++pipelineJobCount); -// verifyReminderPosted(projectName, privateData.get(0), 2); -// verifyReminderPosted(projectName, privateData.get(0), 1); - // Change the reminder frequency to 1. log("Changing reminder settings. Setting reminder frequency to 1."); - saveSettings("2", "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); - String message = String.format("Skipping reminder for experiment Id %d - Recent reminder already sent", privateData.get(0).getExperimentAnnotationsId()); + 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 experiments "); // Change reminder frequency to 0 again. log("Changing reminder settings. Setting reminder frequency to 0."); - saveSettings("2", "0"); + saveSettings("2", "0", "0"); // Request extension for the first experiment. log("Requesting extension for experiment Id " + privateData.get(0).getExperimentAnnotationsId()); @@ -187,7 +194,7 @@ private void testSendingReminders(String projectName, List dataF 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. - String message2 = String.format("Skipping reminder for experiment Id %d - Submitter has requested deletion", privateData.get(1).getExperimentAnnotationsId()); + 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 "); } @@ -275,6 +282,7 @@ private void verifyReminderPosted(String projectName, List folde verifyReminderPosted(projectName, folderInfo, count); } } + private void verifyReminderPosted(String projectName, DataFolderInfo folderInfo, int count) { goToProjectFolder(projectName, folderInfo.getTargetFolder()); @@ -291,21 +299,23 @@ private void verifyReminderPosted(String projectName, DataFolderInfo folderInfo, } } - private void saveSettings(String extensionLength, String reminderFrequency) + 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("extensionLength"), extensionLength); + setFormElement(Locator.input("delayUntilFirstReminder"), delayUntilFirstReminder); setFormElement(Locator.input("reminderFrequency"), reminderFrequency); + setFormElement(Locator.input("extensionLength"), extensionLength); clickButton("Save"); waitForText("Private data message settings saved"); clickAndWait(Locator.linkWithText("Back to Panorama Public Admin Console")); clickAndWait(Locator.linkWithText("Private Data Reminder Settings")); - assertEquals(String.valueOf(extensionLength), getFormElement(Locator.input("extensionLength"))); + 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) From 3794b8b50ac58e3319e1d7b5c6d4696c537fbbb3 Mon Sep 17 00:00:00 2001 From: vagisha Date: Thu, 14 Aug 2025 13:48:51 -0700 Subject: [PATCH 09/18] - Fixed shortURL column in DatasetStatusTableInfo. - Some cleanup. --- .../PanoramaPublicController.java | 465 +++++++++--------- .../panoramapublic/PanoramaPublicModule.java | 6 - .../PanoramaPublicNotification.java | 1 - .../query/DatasetStatusTableInfo.java | 8 +- .../PrivateDataReminderTest.java | 5 +- 5 files changed, 237 insertions(+), 248 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index dd4f4540..23e127f3 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10085,241 +10085,6 @@ public static ActionURL getViewExperimentModificationsURL(int experimentAnnotati return result; } - @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() throws Exception; - - @Override - public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) - { - _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, errors); - if (_exptAnnotations == null) - { - return; - } - - ensureCorrectContainer(getContainer(), _exptAnnotations.getContainer(), getViewContext()); - - ShortURLRecord shortUrl = _exptAnnotations.getShortUrl(); - _datasetStatus = DatasetStatusManager.getForShortUrl(shortUrl); - - // Action-specific validation - doValidationForAction(errors); - } - - @Override - public boolean handlePost(ShortUrlForm shortUrlForm, BindException errors) throws Exception - { - DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(_exptAnnotations.getShortUrl()); - try(DbScope.Transaction transaction = CoreSchema.getInstance().getSchema().getScope().ensureTransaction()) - { - if (datasetStatus == null) - { - datasetStatus = new DatasetStatus(); - datasetStatus.setShortUrl(_exptAnnotations.getShortUrl()); - updateDatasetStatus(datasetStatus); - DatasetStatusManager.save(datasetStatus, getUser()); - } - else - { - updateDatasetStatus(datasetStatus); - DatasetStatusManager.update(datasetStatus, getUser()); - } - - _datasetStatus = datasetStatus; - - // Post notification - 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 - public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception - { - setTitle("Request Extension"); - HtmlView view = new HtmlView(DIV( - DIV("You are requesting an extension for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), - DIV("Title: " + _exptAnnotations.getTitle()), - DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), - DIV("Submitter: " + _exptAnnotations.getSubmitterName()) - )); - view.setTitle("Request Extension For Panorama Public Data"); - return view; - } - - @Override - protected void doValidationForAction(Errors errors) - { - PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); - if (_datasetStatus != null) - { - if (_datasetStatus.isExtensionCurrent(settings)) - { - errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + _datasetStatus.getShortUrl().renderShortURL() - + ". The extension is valid until " + _datasetStatus.extensionValidUntilFormatted(settings)); - } - else if (_datasetStatus.deletionRequested()) - { - errors.reject(ERROR_MSG, "A deletion request was submitted on " + _datasetStatus.getDeletionRequestedDate() - + " for the data with short URL " + _datasetStatus.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 " + _datasetStatus.extensionValidUntilFormatted(settings)), - BR(), - DIV( - LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer())) - ) - )); - } - } - - @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class}) - public class RequestDeletionAction extends UpdateDatasetStatusAction - { - @Override - public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception - { - setTitle("Request Deletion"); - HtmlView view = new HtmlView(DIV( - DIV("You are requesting deletion of the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), - DIV("Title: " + _exptAnnotations.getTitle()), - DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), - DIV("Submitter: " + _exptAnnotations.getSubmitterName()) - )); - view.setTitle("Request Deletion For Panorama Public Data"); - return view; - } - - @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 " + _datasetStatus.getShortUrl().renderShortURL()); - } - } - } - - @Override - protected void updateDatasetStatus(DatasetStatus datasetStatus) - { - datasetStatus.setDeletionRequestedDate(new Date()); - } - - @Override - protected void postNotification() throws Exception - { - // 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(new ButtonBuilder("Home").submit(false).href(AppProps.getInstance().getHomePageActionURL())) - )); - } - } - - 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; - } - - @AdminConsoleAction @RequiresPermission(AdminOperationsPermission.class) public static class PrivateDataReminderSettingsAction extends FormViewAction @@ -10554,6 +10319,236 @@ public void setExperiments(List experiments) } } + @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() throws Exception; + + @Override + public void validateCommand(ShortUrlForm shortUrlForm, Errors errors) + { + _exptAnnotations = getValidExperimentAnnotations(shortUrlForm, errors); + if (_exptAnnotations == null) + { + return; + } + + ensureCorrectContainer(getContainer(), _exptAnnotations.getContainer(), getViewContext()); + + _datasetStatus = DatasetStatusManager.getForShortUrl(_exptAnnotations.getShortUrl()); + + // 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.setShortUrl(_exptAnnotations.getShortUrl()); + updateDatasetStatus(_datasetStatus); + DatasetStatusManager.save(_datasetStatus, getUser()); + } + else + { + updateDatasetStatus(_datasetStatus); + DatasetStatusManager.update(_datasetStatus, getUser()); + } + + // Post notification + 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 + public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception + { + setTitle("Request Extension"); + HtmlView view = new HtmlView(DIV( + DIV("You are requesting an extension for the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), + DIV("Title: " + _exptAnnotations.getTitle()), + DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), + DIV("Submitter: " + _exptAnnotations.getSubmitterName()) + )); + view.setTitle("Request Extension For Panorama Public Data"); + return view; + } + + @Override + protected void doValidationForAction(Errors errors) + { + if (_datasetStatus != null) + { + PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); + if (_datasetStatus.isExtensionCurrent(settings)) + { + errors.reject(ERROR_MSG, "An extension has already been requested for the data with short URL " + _datasetStatus.getShortUrl().renderShortURL() + + ". The extension is valid until " + _datasetStatus.extensionValidUntilFormatted(settings)); + } + else if (_datasetStatus.deletionRequested()) + { + errors.reject(ERROR_MSG, "A deletion request was submitted on " + _datasetStatus.getDeletionRequestedDate() + + " for the data with short URL " + _datasetStatus.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 " + _datasetStatus.extensionValidUntilFormatted(settings)), + BR(), + DIV( + LinkBuilder.labkeyLink("Data Folder", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(_exptAnnotations.getContainer())) + ) + )); + } + } + + @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class}) + public class RequestDeletionAction extends UpdateDatasetStatusAction + { + @Override + public ModelAndView getConfirmView(ShortUrlForm shortUrlForm, BindException errors) throws Exception + { + setTitle("Request Deletion"); + HtmlView view = new HtmlView(DIV( + DIV("You are requesting deletion of the private data on Panorama Public at " + _exptAnnotations.getShortUrl().renderShortURL()), + DIV("Title: " + _exptAnnotations.getTitle()), + DIV("Submitted on: " + DateUtil.formatDateTime(_exptAnnotations.getCreated(), "MMMM d, yyyy")), + DIV("Submitter: " + _exptAnnotations.getSubmitterName()) + )); + view.setTitle("Request Deletion For Panorama Public Data"); + return view; + } + + @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 " + _datasetStatus.getShortUrl().renderShortURL()); + } + } + } + + @Override + protected void updateDatasetStatus(DatasetStatus datasetStatus) + { + datasetStatus.setDeletionRequestedDate(new Date()); + } + + @Override + protected void postNotification() throws Exception + { + // 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(new ButtonBuilder("Home").submit(false).href(AppProps.getInstance().getHomePageActionURL())) + )); + } + } + + 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); diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java index d12ff400..918261f0 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java @@ -356,12 +356,6 @@ public Set getSchemaNames() return Collections.singleton(PanoramaPublicSchema.SCHEMA_NAME); } - @Override - public void startBackgroundThreads() - { - // PrivateDataMessageScheduler.getInstance().initialize(); - } - @NotNull @Override public Set getIntegrationTests() diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java index 55bb7f89..2b61e017 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -459,7 +459,6 @@ this data (available at __PH__DATA__SHORT__URL__), we ask that you do so as soon public static String PLACEHOLDER_RESPOND_TO_MESSAGE_URL = PLACEHOLDER + "RESPOND__TO__MESSAGE__URL__"; public static String PLACEHOLDER_MAKE_DATA_PUBLIC_URL = PLACEHOLDER + "MAKE__DATA__PUBLIC__URL__"; public static String PLACEHOLDER_SHORT_URL = PLACEHOLDER + "DATA__SHORT__URL__"; - public static String replaceLinkPlaceholders(@NotNull String text, @NotNull ExperimentAnnotations expAnnotations, @NotNull Announcement announcement, @NotNull Container announcementContainer) { diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java index c72b58d1..c36b1d70 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/DatasetStatusTableInfo.java @@ -22,9 +22,11 @@ public DatasetStatusTableInfo(@NotNull PanoramaPublicSchema userSchema, Containe new ContainerJoin("ShortUrl", PanoramaPublicManager.getTableInfoExperimentAnnotations(), "ShortUrl")); - var accessUrlCol = wrapColumn("ShortUrl", getRealTable().getColumn("ShortUrl")); - accessUrlCol.setDisplayColumnFactory(new ShortUrlDisplayColumnFactory()); - addColumn(accessUrlCol); + var accessUrlCol = getMutableColumn(FieldKey.fromParts("ShortUrl")); + if (accessUrlCol != null) + { + accessUrlCol.setDisplayColumnFactory(new ShortUrlDisplayColumnFactory()); + } SQLFragment expColSql = new SQLFragment(" (SELECT Id FROM ") .append(PanoramaPublicManager.getTableInfoExperimentAnnotations(), "exp") diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index c5882c97..de2ecc89 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -143,9 +143,8 @@ private void testSendingReminders(String projectName, List dataF postReminders(projectName, false, privateDataCount, -1, ++pipelineJobCount); // Verify that no reminders posted verifyNoReminderPosted(projectName, dataFolderInfos); - var reminderDueDate = LocalDate.now().plusMonths(12).format(DateTimeFormatter.ofPattern("MMMM d, yyyy")); - String message = String.format("Skipping reminder for experiment Id %d - First reminder not due until %s", privateData.get(0).getExperimentAnnotationsId(), reminderDueDate); - String message2 = String.format("Skipping reminder for experiment Id %d - First reminder not due until %s", privateData.get(1).getExperimentAnnotationsId(), reminderDueDate); + 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."); From 19fc8e5c84da81ff292b40f21f63e971eaf0a976 Mon Sep 17 00:00:00 2001 From: vagisha Date: Thu, 14 Aug 2025 15:43:23 -0700 Subject: [PATCH 10/18] - Fix form validation, nav trails. --- .../PanoramaPublicController.java | 117 ++++++++++++------ .../view/privateDataRemindersSettingsForm.jsp | 1 - 2 files changed, 78 insertions(+), 40 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 23e127f3..c75e8a4c 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -389,7 +389,7 @@ private ModelAndView getPrivateDataReminderSettingsLink() @Override public void addNavTrail(NavTree root) { - PageFlowUtil.urlProvider(AdminUrls.class).addAdminNavTrail(root, "Panorama Public Admin Console", getClass(), getContainer()); + addPanoramaPublicAdminConsoleNav(root, getContainer()); } } @@ -526,7 +526,7 @@ public void addNavTrail(NavTree root) private static void addPanoramaPublicAdminConsoleNav(NavTree root, Container container) { - root.addChild("Panorama Public Admin Console", new ActionURL(PanoramaPublicAdminViewAction.class, container)); + PageFlowUtil.urlProvider(AdminUrls.class).addAdminNavTrail(root, "Panorama Public Admin Console", PanoramaPublicAdminViewAction.class, container); } public static class CreateJournalGroupForm @@ -1574,6 +1574,7 @@ public ModelAndView getSuccessView(ManageCatalogEntryForm form) @Override public void addNavTrail(NavTree root) { + addPanoramaPublicAdminConsoleNav(root, getContainer()); root.addChild("Panorama Public Catalog Settings"); } } @@ -10085,12 +10086,37 @@ public static ActionURL getViewExperimentModificationsURL(int experimentAnnotati return result; } - @AdminConsoleAction @RequiresPermission(AdminOperationsPermission.class) public static class PrivateDataReminderSettingsAction extends FormViewAction { @Override - public void validateCommand(PrivateDataReminderSettingsForm form, Errors errors) {} + 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' must be greater 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' must be greater 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' must be greater than 0."); + } + } @Override public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow, BindException errors) throws Exception @@ -10105,7 +10131,7 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean reshow } VBox view = new VBox(); - view.addView(new JspView<>("/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp", form)); + 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; @@ -10144,7 +10170,7 @@ public ModelAndView getSuccessView(PrivateDataReminderSettingsForm form) @Override public void addNavTrail(NavTree root) { - PageFlowUtil.urlProvider(AdminUrls.class).addAdminNavTrail(root, "Panorama Public Admin Console", PanoramaPublicAdminViewAction.class, getContainer()); + addPanoramaPublicAdminConsoleNav(root, getContainer()); root.addChild("Private Data Reminder Settings"); } } @@ -10152,9 +10178,9 @@ public void addNavTrail(NavTree root) public static class PrivateDataReminderSettingsForm { private boolean _enabled; - private int _extensionLength; - private int _reminderFrequency; - private int _delayUntilFirstReminder; + private Integer _extensionLength; + private Integer _reminderFrequency; + private Integer _delayUntilFirstReminder; public boolean isEnabled() { @@ -10166,32 +10192,32 @@ public void setEnabled(boolean enabled) _enabled = enabled; } - public int getExtensionLength() + public Integer getExtensionLength() { return _extensionLength; } - public void setExtensionLength(int extensionLength) + public void setExtensionLength(Integer extensionLength) { _extensionLength = extensionLength; } - public int getReminderFrequency() + public Integer getReminderFrequency() { return _reminderFrequency; } - public void setReminderFrequency(int reminderFrequency) + public void setReminderFrequency(Integer reminderFrequency) { _reminderFrequency = reminderFrequency; } - public int getDelayUntilFirstReminder() + public Integer getDelayUntilFirstReminder() { return _delayUntilFirstReminder; } - public void setDelayUntilFirstReminder(int delayUntilFirstReminder) + public void setDelayUntilFirstReminder(Integer delayUntilFirstReminder) { _delayUntilFirstReminder = delayUntilFirstReminder; } @@ -10225,6 +10251,7 @@ public ModelAndView getView(PrivateDataSendReminderForm form, boolean reshow, Bi 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; } @@ -10257,8 +10284,8 @@ public URLHelper getSuccessURL(PrivateDataSendReminderForm form) @Override public void addNavTrail(NavTree root) { - PageFlowUtil.urlProvider(AdminUrls.class).addAdminNavTrail(root, "Private Data Reminder Settings", PrivateDataReminderSettingsAction.class, ContainerManager.getRoot()); - root.addChild("Send Private Data Reminders"); + root.addChild("Private Data Reminder Settings", new ActionURL(PrivateDataReminderSettingsAction.class, ContainerManager.getRoot())); + root.addChild("Send Reminders"); } } @@ -10327,7 +10354,23 @@ public abstract class UpdateDatasetStatusAction extends ConfirmAction From ef291b52f4b04dbc89f23ab23f24aeb9876423f8 Mon Sep 17 00:00:00 2001 From: vagisha Date: Sat, 16 Aug 2025 14:47:22 -0700 Subject: [PATCH 11/18] - Updated notification messages - Moved date calculations to PrivateDateReminderSettings. Added unit tests - Improvements to PrivateDataReminderJob - Fixed PrivateDataReminderTest --- .../PanoramaPublicController.java | 29 +- .../panoramapublic/PanoramaPublicModule.java | 2 + .../PanoramaPublicNotification.java | 32 +- .../message/PrivateDataMessageScheduler.java | 37 +- .../message/PrivateDataReminderSettings.java | 288 +++++++++- .../panoramapublic/model/DatasetStatus.java | 62 +-- .../pipeline/PrivateDataReminderJob.java | 513 ++++++++++++++---- .../view/privateDataRemindersSettingsForm.jsp | 2 +- .../PrivateDataReminderTest.java | 61 ++- 9 files changed, 757 insertions(+), 269 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index c75e8a4c..259adb9b 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10098,7 +10098,7 @@ public void validateCommand(PrivateDataReminderSettingsForm form, Errors errors) } else if (form.getDelayUntilFirstReminder() < 0) { - errors.reject(ERROR_MSG, "Value for 'Delay until first reminder' must be greater than 0."); + errors.reject(ERROR_MSG, "Value for 'Delay until first reminder' cannot be less than 0."); } if (form.getReminderFrequency() == null) { @@ -10106,7 +10106,7 @@ else if (form.getDelayUntilFirstReminder() < 0) } else if (form.getReminderFrequency() < 0) { - errors.reject(ERROR_MSG, "Value for 'Reminder frequency' must be greater than 0."); + errors.reject(ERROR_MSG, "Value for 'Reminder frequency' cannot be less than 0."); } if (form.getExtensionLength() == null) { @@ -10114,7 +10114,7 @@ else if (form.getReminderFrequency() < 0) } else if (form.getExtensionLength() < 0) { - errors.reject(ERROR_MSG, "Value for 'Extension duration' must be greater than 0."); + errors.reject(ERROR_MSG, "Value for 'Extension duration' cannot be less than 0."); } } @@ -10244,7 +10244,7 @@ public ModelAndView getView(PrivateDataSendReminderForm form, boolean reshow, Bi qSettings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("Public"), "No")); QueryView tableView = new QueryView(new PanoramaPublicSchema(getUser(), getContainer()), qSettings, null); - tableView.setTitle("Private Panorama Public Datasets"); + tableView.setTitle("Private Panorama Private Datasets"); tableView.setFrame(WebPartView.FrameType.NONE); form.setDataRegionName(tableView.getDataRegionName()); @@ -10266,7 +10266,7 @@ public boolean handlePost(PrivateDataSendReminderForm form, BindException errors return false; } PipelineJob job = new PrivateDataReminderJob(getViewBackgroundInfo(), - PipelineService.get().getPipelineRootSetting(ContainerManager.getRoot()), + PipelineService.get().getPipelineRootSetting(getContainer()), JournalManager.getJournal(getContainer()), form.getSelectedExperimentIds(), form.getTestMode()); @@ -10294,7 +10294,6 @@ public static class PrivateDataSendReminderForm private boolean _testMode; private String _selectedIds; private String _dataRegionName = null; - private List _experiments = null; public boolean getTestMode() { @@ -10334,16 +10333,6 @@ public void setDataRegionName(String dataRegionName) { _dataRegionName = dataRegionName; } - - public List getExperiments() - { - return _experiments; - } - - public void setExperiments(List experiments) - { - _experiments = experiments; - } } @RequiresAnyOf({AdminPermission.class, PanoramaPublicSubmitterPermission.class}) @@ -10355,7 +10344,6 @@ public abstract class UpdateDatasetStatusAction extends ConfirmAction 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 2b61e017..471f2153 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -23,7 +23,6 @@ import org.labkey.panoramapublic.datacite.DataCiteException; import org.labkey.panoramapublic.datacite.DataCiteService; 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.JournalExperiment; @@ -306,17 +305,13 @@ public static void postPrivateStatusExtensionMessage(@NotNull Journal journal, @ throw new NotFoundException(String.format("Could not find an admin user for %s.", journal.getName())); } PrivateDataReminderSettings reminderSettings = PrivateDataReminderSettings.get(); - /* - Thank you for your request to extend the private status of your data on Panorama Public at . - Your data has been granted an extension for an additional 6 months. You’ll receive another reminder at that time, or you may make the dataset public earlier. - Please feel free to contact us if you have any questions - */ + 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’ll receive another reminder when this period ends. ") + .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()))) @@ -354,7 +349,7 @@ public static void postDataDeletionRequestMessage(@NotNull Journal journal, @Not 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."); + .append(" may have been deleted."); } messageBody.append(NL2).append("Best regards,"); @@ -369,7 +364,7 @@ public static void postPrivateDataReminderMessage(@NotNull Journal journal, @Not @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 Dataset on Panorama Public"; + String title = "Action Required: Status Update for Your Private Data on Panorama Public"; postNotificationFullTitle(journal, js.getJournalExperiment(), message, messagePoster, title, StatusOption.Closed, notifyUsers); } @@ -377,17 +372,6 @@ public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations @NotNull JournalSubmission js,@NotNull Announcement announcement, @NotNull Container announcementContainer, @NotNull User journalAdmin) { - /* - We are reaching out regarding your dataset on Panorama Public (https://panoramaweb.org/polyjuice.url), which has been private since January 1, 2024. - Is the paper associated with this work already published? - If yes: Please make your data public by clicking the "Make Public" button in your folder or by clicking [Make Data Public] here. This helps ensure that your valuable research is easily accessible to the community. - If not: You have a couple of options: - Request an Extension - If your paper is still under review, or you need additional time, please let us know by clicking [Request Extension] - Delete from Panorama Public - If you no longer wish to host your data on Panorama Public, please click [Request Deletion]. We will remove your dataset from Panorama Public. However, your source folder (/Hogwarts/Gryffindor/magic-potion) will remain intact, allowing you to resubmit your data in the future if you wish. - If you have any questions or need further assistance, please do not hesitate to respond to this message by clicking here. - Thank you for sharing your research on Panorama Public. We appreciate your commitment to open science and your contributions to the research community. - */ - String shortUrl = exptAnnotations.getShortUrl().renderShortURL(); String makePublicLink = PanoramaPublicController.getMakePublicUrl(exptAnnotations.getId(), exptAnnotations.getContainer()).getURIString(); String dateString = DateUtil.formatDateTime(js.getLatestSubmission().getCreated(), "MMMM d, yyyy"); @@ -410,10 +394,10 @@ We are reaching out regarding your dataset on Panorama Public (https://panoramaw StringBuilder message = new StringBuilder(); message.append("Dear ").append(getUserName(submitter)).append(",").append(NL2) - .append("We are reaching out regarding your dataset on Panorama Public (").append(shortUrl).append("), which has been private since ") + .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 ") + .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:") @@ -421,7 +405,7 @@ We are reaching out regarding your dataset on Panorama Public (https://panoramaw .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 dataset from Panorama Public."); + .append("We will remove your data from Panorama Public."); if (sourceExperiment != null) { message.append(" However, your source folder (") @@ -594,7 +578,7 @@ 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 result 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. // 8/13/25 - Escape tilde (~) as well. In the LabKey Markdown flavor, text between // single tildes (e.g., ~strikethrough~) is rendered as strikethrough. diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java index 204fba37..8492e105 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java @@ -9,9 +9,12 @@ import org.labkey.api.pipeline.PipelineService; import org.labkey.api.security.User; import org.labkey.api.util.ConfigurationException; +import org.labkey.api.util.ExceptionUtil; 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.DateBuilder; import org.quartz.Job; import org.quartz.JobBuilder; @@ -88,40 +91,42 @@ protected Trigger getTrigger() public static class PrivateDataMessageSchedulerJob implements Job { - private final @Nullable User _user; - @SuppressWarnings("unused") - public PrivateDataMessageSchedulerJob() - { - this(null); - } - - public PrivateDataMessageSchedulerJob(@Nullable User user) - { - _user = user; - } + public PrivateDataMessageSchedulerJob() {} @Override public void execute(JobExecutionContext context) { try { - Container c = ContainerManager.getRoot(); - ViewBackgroundInfo vbi = new ViewBackgroundInfo(c, _user, null); + 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 root container"); + throw new ConfigurationException("No valid pipeline root found in the container " + c.getName()); } - PipelineJob job = new PrivateDataReminderJob(vbi, PipelineService.get().getPipelineRootSetting(ContainerManager.getRoot()), false); + PipelineJob job = new PrivateDataReminderJob(vbi, PipelineService.get().getPipelineRootSetting(c), false); PipelineService.get().queueJob(job); } catch(Exception e) { _log.error("Error queuing PrivateDataReminderJob", e); - // ExceptionUtil.logExceptionToMothership(null, e); + ExceptionUtil.logExceptionToMothership(null, e); } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java index 5faa6c1f..d76f4c1c 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java @@ -1,6 +1,17 @@ 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.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; public class PrivateDataReminderSettings { @@ -8,21 +19,20 @@ public class PrivateDataReminderSettings public static final String PROP_ENABLE_REMINDER = "Enable private data reminder"; 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_MONTHS = "Extension duration (months)"; - - + public static final String PROP_EXTENSION_LENGTH = "Extension duration (months)"; private static final boolean DEFAULT_ENABLE_REMINDERS = false; 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. + private static final String DATE_FORMAT_PATTERN = "MMMM d, yyyy"; + private boolean _enableReminders; private int _delayUntilFirstReminder; private int _reminderFrequency; private int _extensionLength; - public static PrivateDataReminderSettings get() { PropertyManager.WritablePropertyMap settingsMap = PropertyManager.getWritableProperties(PROP_PRIVATE_DATA_REMINDER, false); @@ -30,13 +40,24 @@ public static PrivateDataReminderSettings get() PrivateDataReminderSettings settings = new PrivateDataReminderSettings(); if(settingsMap != null) { - boolean enableReminders = settingsMap.get(PROP_EXTENSION_MONTHS) == null ? DEFAULT_ENABLE_REMINDERS : Boolean.valueOf(settingsMap.get(PROP_ENABLE_REMINDER)); + 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)); + + 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_EXTENSION_LENGTH : Integer.valueOf(settingsMap.get(PROP_REMINDER_FREQUENCY)); + + 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_MONTHS) == null ? DEFAULT_EXTENSION_LENGTH : Integer.valueOf(settingsMap.get(PROP_EXTENSION_MONTHS)); + + int extensionLength = settingsMap.get(PROP_EXTENSION_LENGTH) == null + ? DEFAULT_EXTENSION_LENGTH + : Integer.valueOf(settingsMap.get(PROP_EXTENSION_LENGTH)); settings.setExtensionLength(extensionLength); } else @@ -56,7 +77,7 @@ public static void save(PrivateDataReminderSettings settings) 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_MONTHS, String.valueOf(settings.getExtensionLength())); + settingsMap.put(PROP_EXTENSION_LENGTH, String.valueOf(settings.getExtensionLength())); settingsMap.save(); } @@ -99,4 +120,253 @@ 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); + } + + public 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 index 0fbace88..ce7c2177 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java +++ b/panoramapublic/src/org/labkey/panoramapublic/model/DatasetStatus.java @@ -1,13 +1,10 @@ package org.labkey.panoramapublic.model; import org.jetbrains.annotations.Nullable; -import org.labkey.api.util.DateUtil; import org.labkey.api.view.ShortURLRecord; import org.labkey.panoramapublic.message.PrivateDataReminderSettings; -import java.time.ZoneId; import java.util.Date; -import java.time.LocalDate; public class DatasetStatus extends DbEntity { @@ -53,7 +50,7 @@ public Date getDeletionRequestedDate() public @Nullable String getDeletionRequestedDateFormatted() { - return format(_deletionRequestedDate); + return PrivateDataReminderSettings.format(_deletionRequestedDate); } public void setDeletionRequestedDate(Date deletionRequestedDate) @@ -75,61 +72,4 @@ public boolean reminderSent() { return _lastReminderDate != null; } - - public boolean isExtensionCurrent(PrivateDataReminderSettings settings) - { - if (_extensionRequestedDate == null) - { - return false; - } - - LocalDate extensionDate = _extensionRequestedDate.toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDate(); - - LocalDate extensionValidStartDate = LocalDate.now().minusMonths(settings.getExtensionLength()); - - return extensionDate.isAfter(extensionValidStartDate); - } - - public boolean isLastReminderRecent(PrivateDataReminderSettings settings) - { - if (_lastReminderDate == null) - { - return false; - } - - LocalDate reminderDate = _lastReminderDate.toInstant() - .atZone(ZoneId.systemDefault()) - .toLocalDate(); - - LocalDate reminderValidStartDate = LocalDate.now().minusMonths(settings.getReminderFrequency()); - - return reminderDate.isAfter(reminderValidStartDate); - } - - public @Nullable Date getExtensionValidUntilDate(PrivateDataReminderSettings settings) - { - if (_extensionRequestedDate == null) - { - return null; - } - - return Date.from( - _extensionRequestedDate.toInstant() - .atZone(ZoneId.systemDefault()) - .plusMonths(settings.getExtensionLength()) - .toInstant() - ); - } - - public @Nullable String extensionValidUntilFormatted(PrivateDataReminderSettings settings) - { - return format(getExtensionValidUntilDate(settings)); - } - - private @Nullable String format(Date date) - { - return date != null ? DateUtil.formatDateTime(date, "MMMM d, yyyy") : null; - } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index e599030b..f92682a1 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -1,6 +1,7 @@ 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; @@ -12,7 +13,6 @@ import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.portal.ProjectUrls; import org.labkey.api.security.User; -import org.labkey.api.util.DateUtil; import org.labkey.api.util.FileUtil; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.util.URLHelper; @@ -29,7 +29,6 @@ import org.labkey.panoramapublic.query.JournalManager; import org.labkey.panoramapublic.query.SubmissionManager; -import java.time.Instant; import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; @@ -58,7 +57,7 @@ public PrivateDataReminderJob(ViewBackgroundInfo info, @NotNull PipeRoot root, b public PrivateDataReminderJob(ViewBackgroundInfo info, @NotNull PipeRoot root, Journal panoramaPublic, List experimentAnnotationsIds, boolean test) { super("Panorama Public", info, root); - setLogFile(root.getRootNioPath().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-private-data-reminder", "log"))); + setLogFile(root.getRootFileLike().toNioPathForWrite().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-private-data-reminder", "log"))); _panoramaPublic = panoramaPublic; _experimentAnnotationsIds = experimentAnnotationsIds; @@ -76,11 +75,13 @@ public static List getPrivateDatasets(Journal panoramaPublic) Set subFolders = ContainerManager.getAllChildren(panoramaPublic.getProject()); List privateDataIds = new ArrayList<>(); - PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); for (Container folder : subFolders) { ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder); - privateDataIds.add(exptAnnotations.getId()); + if (exptAnnotations != null) + { + privateDataIds.add(exptAnnotations.getId()); + } } return privateDataIds; @@ -90,12 +91,12 @@ private static ReminderDecision getReminderDecision(@NotNull ExperimentAnnotatio { if (exptAnnotations.isPublic()) { - return ReminderDecision.skip("Data is already public"); + return ReminderDecision.skip("Data is already public."); } if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) { - return ReminderDecision.skip("Not the current version of the experiment"); + return ReminderDecision.skip("Not the current version of the experiment."); } DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(exptAnnotations.getShortUrl()); @@ -103,17 +104,17 @@ private static ReminderDecision getReminderDecision(@NotNull ExperimentAnnotatio { if (datasetStatus.deletionRequested()) { - return ReminderDecision.skip("Submitter has requested deletion"); + return ReminderDecision.skip("Submitter has requested deletion."); } - if (datasetStatus.isExtensionCurrent(settings)) + if (settings.isExtensionValid(datasetStatus)) { return ReminderDecision.skip("Submitter requested an extension. Extension is current."); } - if (datasetStatus.isLastReminderRecent(settings)) + if (settings.isLastReminderRecent(datasetStatus)) { - return ReminderDecision.skip("Recent reminder already sent"); + return ReminderDecision.skip("Recent reminder already sent."); } } return reminderIsDue(exptAnnotations, settings); @@ -128,7 +129,7 @@ private static ReminderDecision reminderIsDue(ExperimentAnnotations exptAnnotati 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("MMMM d, yyyy")))); + return ReminderDecision.skip(String.format("First reminder not due until %s.", firstReminderDate.format(DateTimeFormatter.ofPattern("MMMM d, yyyy")))); } return ReminderDecision.post(); } @@ -161,7 +162,7 @@ public void run() if (_panoramaPublic == null) { - getLogger().error("Panorama Public project does not exist"); + getLogger().error("Panorama Public project does not exist."); return; } @@ -178,151 +179,447 @@ private void postMessage(List expAnnotationIds, Journal panoramaPublic) getLogger().info("No private datasets were found."); return; } - getLogger().info(String.format("Posting reminder message to: %d message threads", expAnnotationIds.size())); + Logger log = getLogger(); - int done = 0; + ProcessingContext context = ProcessingContext.create(panoramaPublic, getUser(), _test); + if(!context.isValid()) + { + context.logErrors(log); + return; + } + ProcessingResults processingResults = new ProcessingResults(expAnnotationIds.size(), log); - PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); + processExperiments(_experimentAnnotationsIds, context, processingResults, log); - AnnouncementService announcementSvc = AnnouncementService.get(); + } + + 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(); + } - List experimentNotFound = new ArrayList<>(); - List submissionNotFound = new ArrayList<>(); - List announcementNotFound = new ArrayList<>(); - List submitterNotFound = new ArrayList<>(); - int skipped = 0; + processingResults.logResults(log); + } - Container announcementsFolder = panoramaPublic.getSupportContainer(); - if (announcementsFolder == null) + private void processExperiment(Integer experimentAnnotationsId, ProcessingContext context, ProcessingResults processingResults) + { + ExperimentAnnotations expAnnotations = ExperimentAnnotationsManager.get(experimentAnnotationsId); + if (expAnnotations == null) { - getLogger().error(String.format("%s does not have a support folder for messages.", panoramaPublic.getName())); + processingResults.addExperimentNotFound(experimentAnnotationsId); return; } - User journalAdmin = JournalManager.getJournalAdminUser(panoramaPublic); - if (journalAdmin == null) + ReminderDecision decision = getReminderDecision(expAnnotations, context.getSettings()); + if (!decision.shouldPost()) { - getLogger().error(String.format("Could not find an admin user for %s.", panoramaPublic.getName())); + 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; } - Set exptIds = new HashSet<>(expAnnotationIds); + Container announcementsFolder = context.getAnnouncementsFolder(); + Announcement announcement = context.getAnnouncementService().getAnnouncement(announcementsFolder, getUser(), submission.getAnnouncementId()); + if (announcement == null) + { + processingResults.addAnnouncementNotFound(experimentAnnotationsId, submission, announcementsFolder); + return; + } - try (DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction()) + User submitter = expAnnotations.getSubmitterUser(); + if (submitter == null) { - if (_test) + 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.getForShortUrl(expAnnotations.getShortUrl()); + if (datasetStatus == null) + { + datasetStatus = new DatasetStatus(); + datasetStatus.setShortUrl(expAnnotations.getShortUrl()); + 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) { - getLogger().info("RUNNING IN TEST MODE - MESSAGES WILL NOT BE POSTED."); + log.error(error); } - for (Integer experimentAnnotationsId : exptIds) + } + + 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) { - ExperimentAnnotations expAnnotations = ExperimentAnnotationsManager.get(experimentAnnotationsId); - if (expAnnotations == null) + this.settings = settings; + if (settings == null) { - getLogger().error("Could not find an experiment with Id: " + experimentAnnotationsId); - experimentNotFound.add(experimentAnnotationsId); - continue; + creationErrors.add("Could not load PrivateDataReminderSettings."); } + return this; + } - ReminderDecision decision = getReminderDecision(expAnnotations, settings); - if (!decision.shouldPost()) + public Builder withAnnouncementService(@Nullable AnnouncementService announcementService) + { + this.announcementService = announcementService; + if (announcementService == null) { - getLogger().info("Skipping reminder for experiment Id " + experimentAnnotationsId + " - " + decision.getReason()); - skipped++; - continue; + creationErrors.add("Could not get AnnouncementService."); } - JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(expAnnotations); - if (submission == null || submission.getLatestSubmission() == null) + return this; + } + + public Builder withAnnouncementsFolder(@Nullable Container announcementsFolder) + { + this.announcementsFolder = announcementsFolder; + if (announcementsFolder == null) { - getLogger().error("Could not find a submission request for experiment Id: " + experimentAnnotationsId); - submissionNotFound.add(experimentAnnotationsId); - continue; + creationErrors.add("Announcements folder is null - Panorama Public project does not have a support folder for messages."); } + return this; + } - Announcement announcement = announcementSvc.getAnnouncement(announcementsFolder, getUser(), submission.getAnnouncementId()); - if (announcement == null) + public Builder withJournalAdmin(@Nullable User journalAdmin) + { + this.journalAdmin = journalAdmin; + if (journalAdmin == null) { - getLogger().error("Could not find the message thread for experiment Id: " + experimentAnnotationsId - + "; announcement Id: " + submission.getAnnouncementId() + " in the folder " + announcementsFolder.getPath()); - announcementNotFound.add(experimentAnnotationsId); - continue; + creationErrors.add("Could not find an admin user for the Panorama Public project."); } + return this; + } - User submitter = expAnnotations.getSubmitterUser(); - if (submitter == null) + public Builder withCurrentUser(@Nullable User currentUser) + { + this.currentUser = currentUser; + if (currentUser == null) { - getLogger().error("Could not find a submitter user for experiment Id: " + experimentAnnotationsId); - submitterNotFound.add(experimentAnnotationsId); - continue; + creationErrors.add("Current user is null."); } + return this; + } - if (!_test) + public Builder withJournal(@Nullable Journal journal) + { + this.journal = journal; + if (journal == null) { - // 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(panoramaPublic, submission, expAnnotations, - submitter, getUser(), notifyList, announcement, announcementsFolder, journalAdmin); - - DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(expAnnotations.getShortUrl()); - if (datasetStatus == null) - { - datasetStatus = new DatasetStatus(); - datasetStatus.setShortUrl(expAnnotations.getShortUrl()); - datasetStatus.setLastReminderDate(Date.from(Instant.now())); - DatasetStatusManager.save(datasetStatus, getUser()); - } - else - { - datasetStatus.setLastReminderDate(Date.from(Instant.now())); - DatasetStatusManager.update(datasetStatus, getUser()); - } + creationErrors.add("Panorama Public journal is null."); } + return this; + } - getLogger().info(String.format("Experiment ID: %d; Announcement ID %d; Short URL: %s.", - experimentAnnotationsId, announcement.getRowId(), expAnnotations.getShortUrl().renderShortURL())); - getLogger().info(String.format("Folder: %s", PageFlowUtil.urlProvider(ProjectUrls.class).getBeginURL(expAnnotations.getContainer()).getURIString())); - getLogger().info(String.format("Completed: %d of %d", ++done, total)); + 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; } - transaction.commit(); + + 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(); } + } - if (!experimentNotFound.isEmpty()) + 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) { - getLogger().error("Experiments with the following Ids could not be found: " + StringUtils.join(experimentNotFound, ", ")); + _total = totalExperiments; + _log = log; } - if (!submissionNotFound.isEmpty()) + + public void addExperimentNotFound(Integer experimentId) { - getLogger().error("Submission requests were not found for the following experiment Ids: " + StringUtils.join(submissionNotFound, ", ")); + _experimentNotFound.add(experimentId); + _log.error(String.format("Could not find an experiment with Id: %s.", experimentId)); } - if (!announcementNotFound.isEmpty()) + + public void addSubmissionNotFound(Integer experimentId) { - getLogger().error("Support message threads were not found for the following experiment Ids: " + StringUtils.join(announcementNotFound, ", ")); + _submissionNotFound.add(experimentId); + _log.error(String.format("Could not find a submission request for experiment Id: %s.", experimentId)); } - if (!submitterNotFound.isEmpty()) + public void addLatestSubmissionNotFound(Integer experimentId) { - getLogger().error("Submitter user was not found for the following experiment Ids: " + StringUtils.join(submitterNotFound, ", ")); + _submissionNotFound.add(experimentId); + _log.error(String.format("Submission found but latest submission is null for experiment Id: %s.", experimentId)); } - if (skipped > 0) + + public void addAnnouncementNotFound(Integer experimentId, JournalSubmission submission, Container announcementsFolder) { - getLogger().info("Skipped posting reminders for " + skipped + " experiments "); + _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())); } - } - @Override - public URLHelper getStatusHref() - { - return null; - } + public void addSubmitterNotFound(Integer experimentId) + { + _submitterNotFound.add(experimentId); + _log.error(String.format("Could not find a submitter user for experiment Id: %s.", experimentId)); + } - @Override - public String getDescription() - { - return "Post private data reminder messages"; + 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("Skipped posting reminders for " + _skipped.size() + " experiments"); + } + + if (_processed > 0) + { + log.info("Successfully processed " + _processed + " experiments"); + } + + 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/view/privateDataRemindersSettingsForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp index a9c63357..740787e9 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp @@ -97,7 +97,7 @@
- <%=h(PrivateDataReminderSettings.PROP_EXTENSION_MONTHS)%> + <%=h(PrivateDataReminderSettings.PROP_EXTENSION_LENGTH)%> diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index de2ecc89..84f60836 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -6,12 +6,11 @@ import org.labkey.test.Locator; import org.labkey.test.categories.External; import org.labkey.test.categories.MacCossLabModules; +import org.labkey.test.components.panoramapublic.TargetedMsExperimentWebPart; import org.labkey.test.pages.LabkeyErrorPage; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; import java.util.List; import static org.junit.Assert.assertEquals; @@ -29,7 +28,7 @@ public class PrivateDataReminderTest extends PanoramaPublicBaseTest 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 Dataset on Panorama Public"; + 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 - "; @@ -38,7 +37,7 @@ public class PrivateDataReminderTest extends PanoramaPublicBaseTest @Test public void testPrivateDataReminder() { -// String panoramaPublicProject = "Panorama Public 2"; +// String panoramaPublicProject = "Panorama Public 2"; String panoramaPublicProject = PANORAMA_PUBLIC; goToProjectHome(panoramaPublicProject); ApiPermissionsHelper permissionsHelper = new ApiPermissionsHelper(this); @@ -145,7 +144,7 @@ private void testSendingReminders(String projectName, List dataF 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 "); + 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"); @@ -170,7 +169,7 @@ private void testSendingReminders(String projectName, List dataF 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 experiments "); + verifyPipelineJobLogMessage(projectName, message, "Skipped posting reminders for 1 experiments"); // Change reminder frequency to 0 again. log("Changing reminder settings. Setting reminder frequency to 0."); @@ -182,9 +181,9 @@ private void testSendingReminders(String projectName, List dataF 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.", + 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 experiments "); + verifyPipelineJobLogMessage(projectName, message, "Skipped posting reminders for 1 experiments"); // Request deletion for the second experiment. log("Requesting deletion for experiment Id " + privateData.get(1).getExperimentAnnotationsId()); @@ -194,7 +193,7 @@ private void testSendingReminders(String projectName, List dataF 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 "); + verifyPipelineJobLogMessage(projectName, message, message2, "Skipped posting reminders for 2 experiments"); } private void requestExtension(String projectName, DataFolderInfo folderInfo) @@ -202,21 +201,18 @@ private void requestExtension(String projectName, DataFolderInfo folderInfo) goToProjectFolder(projectName, folderInfo.getTargetFolder()); gotoSupportMessage(folderInfo); - impersonate(SUBMITTER_3); - click(Locator.linkWithText("Request Extension")); - new LabkeyErrorPage(getDriver()).assertUnauthorized(checker()); - - stopImpersonating(); + String actionName = "Request Extension"; + checkUnauthorizedAccess(actionName); goToProjectFolder(projectName, folderInfo.getTargetFolder()); gotoSupportMessage(folderInfo); impersonate(folderInfo.getSubmitter()); - assertTextPresent("Request Extension"); - click(Locator.linkWithText("Request Extension")); + 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"); + clickButton("OK", 0); waitForText("An extension request was successfully submitted for the data at " + folderInfo.getShortUrl()); stopImpersonating(); @@ -230,21 +226,18 @@ private void requestDeletion(String projectName, DataFolderInfo folderInfo) goToProjectFolder(projectName, folderInfo.getTargetFolder()); gotoSupportMessage(folderInfo); - impersonate(SUBMITTER_3); - click(Locator.linkWithText("Request Deletion")); - new LabkeyErrorPage(getDriver()).assertUnauthorized(checker()); - - stopImpersonating(); + String actionName = "Request Deletion"; + checkUnauthorizedAccess(actionName); goToProjectFolder(projectName, folderInfo.getTargetFolder()); gotoSupportMessage(folderInfo); impersonate(folderInfo.getSubmitter()); - assertTextPresent("Request Deletion"); - click(Locator.linkWithText("Request Deletion")); + 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"); + clickButton("OK", 0); waitForText("A deletion request was successfully submitted for the data at " + folderInfo.getShortUrl()); stopImpersonating(); @@ -253,6 +246,16 @@ private void requestDeletion(String projectName, DataFolderInfo 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); @@ -307,7 +310,7 @@ private void saveSettings(String extensionLength, String delayUntilFirstReminder setFormElement(Locator.input("delayUntilFirstReminder"), delayUntilFirstReminder); setFormElement(Locator.input("reminderFrequency"), reminderFrequency); setFormElement(Locator.input("extensionLength"), extensionLength); - clickButton("Save"); + clickButton("Save", 0); waitForText("Private data message settings saved"); clickAndWait(Locator.linkWithText("Back to Panorama Public Admin Console")); @@ -355,11 +358,11 @@ private void postReminders(String projectName, boolean testMode, int expectedExp checkCheckbox(Locator.checkboxByName("testMode")); } - clickButton("Post Reminders"); + clickButton("Post Reminders", 0); if (selectRowCount == 0) { - assertTextPresent("Please select at least one experiment"); + waitForText("Please select at least one experiment"); if (testMode) { assertChecked(Locator.checkboxByName("testMode")); @@ -387,7 +390,7 @@ private void goToSendRemindersPage(String projectName) clickAndWait(Locator.linkWithText("Private Data Reminder Settings")); selectOptionByText(Locator.name("journal"), projectName); click(Locator.linkWithText("Send Reminders Now")); - waitForText("Send Private Data Reminders"); + waitForText("Send Reminders"); assertEquals("/" + projectName, getCurrentContainerPath()); } From e1aec152c4c533e31af8bf3f28952128dff88296 Mon Sep 17 00:00:00 2001 From: vagisha Date: Sat, 16 Aug 2025 16:25:14 -0700 Subject: [PATCH 12/18] Fixed pipeline job log messages. --- .../message/PrivateDataMessageScheduler.java | 14 +++----------- .../pipeline/PrivateDataReminderJob.java | 19 ++++++++++--------- .../PrivateDataReminderTest.java | 18 +++--------------- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java index 8492e105..5c456190 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java @@ -1,9 +1,7 @@ package org.labkey.panoramapublic.message; import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.Nullable; import org.labkey.api.data.Container; -import org.labkey.api.data.ContainerManager; import org.labkey.api.pipeline.PipeRoot; import org.labkey.api.pipeline.PipelineJob; import org.labkey.api.pipeline.PipelineService; @@ -15,14 +13,13 @@ import org.labkey.panoramapublic.model.Journal; import org.labkey.panoramapublic.pipeline.PrivateDataReminderJob; import org.labkey.panoramapublic.query.JournalManager; -import org.quartz.DateBuilder; +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.SimpleScheduleBuilder; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; @@ -77,15 +74,10 @@ public void initialize(boolean enable) protected Trigger getTrigger() { - // 1st of every month at 8:00AM -// return TriggerBuilder.newTrigger() -// .withIdentity(TRIGGER_KEY) -// .withSchedule(CronScheduleBuilder.monthlyOnDayAndHourAndMinute(1, 8, 0)) -// .build(); + // Run at 8:00AM every morning return TriggerBuilder.newTrigger() .withIdentity(TRIGGER_KEY) - .withSchedule(SimpleScheduleBuilder.repeatMinutelyForever(2)) - .startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.SECOND)) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(8, 0)) .build(); } diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java index f92682a1..f923245a 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PrivateDataReminderJob.java @@ -15,6 +15,7 @@ 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; @@ -78,7 +79,7 @@ public static List getPrivateDatasets(Journal panoramaPublic) for (Container folder : subFolders) { ExperimentAnnotations exptAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(folder); - if (exptAnnotations != null) + if (exptAnnotations != null && !exptAnnotations.isPublic()) { privateDataIds.add(exptAnnotations.getId()); } @@ -91,12 +92,12 @@ private static ReminderDecision getReminderDecision(@NotNull ExperimentAnnotatio { if (exptAnnotations.isPublic()) { - return ReminderDecision.skip("Data is already public."); + return ReminderDecision.skip("Data is already public"); } if (!ExperimentAnnotationsManager.isCurrentVersion(exptAnnotations)) { - return ReminderDecision.skip("Not the current version of the experiment."); + return ReminderDecision.skip("Not the current version of the experiment"); } DatasetStatus datasetStatus = DatasetStatusManager.getForShortUrl(exptAnnotations.getShortUrl()); @@ -104,17 +105,17 @@ private static ReminderDecision getReminderDecision(@NotNull ExperimentAnnotatio { if (datasetStatus.deletionRequested()) { - return ReminderDecision.skip("Submitter has requested deletion."); + return ReminderDecision.skip("Submitter has requested deletion"); } if (settings.isExtensionValid(datasetStatus)) { - return ReminderDecision.skip("Submitter requested an extension. Extension is current."); + return ReminderDecision.skip("Submitter requested an extension. Extension is current"); } if (settings.isLastReminderRecent(datasetStatus)) { - return ReminderDecision.skip("Recent reminder already sent."); + return ReminderDecision.skip("Recent reminder already sent"); } } return reminderIsDue(exptAnnotations, settings); @@ -129,7 +130,7 @@ private static ReminderDecision reminderIsDue(ExperimentAnnotations exptAnnotati 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("MMMM d, yyyy")))); + return ReminderDecision.skip(String.format("First reminder not due until %s", firstReminderDate.format(DateTimeFormatter.ofPattern("MMMM d, yyyy")))); } return ReminderDecision.post(); } @@ -610,12 +611,12 @@ public void logSummary(Logger log) { if (!_skipped.isEmpty()) { - log.info("Skipped posting reminders for " + _skipped.size() + " experiments"); + log.info(String.format("Skipped posting reminders for %s.", StringUtilsLabKey.pluralize(_skipped.size(), "experiment"))); } if (_processed > 0) { - log.info("Successfully processed " + _processed + " experiments"); + 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", diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index 84f60836..2e9e2960 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -6,7 +6,6 @@ import org.labkey.test.Locator; import org.labkey.test.categories.External; import org.labkey.test.categories.MacCossLabModules; -import org.labkey.test.components.panoramapublic.TargetedMsExperimentWebPart; import org.labkey.test.pages.LabkeyErrorPage; import org.labkey.test.util.ApiPermissionsHelper; import org.labkey.test.util.DataRegionTable; @@ -37,7 +36,6 @@ public class PrivateDataReminderTest extends PanoramaPublicBaseTest @Test public void testPrivateDataReminder() { -// String panoramaPublicProject = "Panorama Public 2"; String panoramaPublicProject = PANORAMA_PUBLIC; goToProjectHome(panoramaPublicProject); ApiPermissionsHelper permissionsHelper = new ApiPermissionsHelper(this); @@ -94,8 +92,6 @@ private DataFolderInfo createAndSubmitFolder(String testProject, String sourceFo String shortAccessUrl = setupFolderSubmitAndCopy(testProject, sourceFolder, targetFolder, experimentTitle, submitter, submitterName, admin, SKY_FILE_1); goToProjectFolder(panoramaPublicProject, targetFolder); -// TargetedMsExperimentWebPart expWebPart = new TargetedMsExperimentWebPart(this); -// String shortAccessUrl = expWebPart.getAccessLink(); goToExperimentDetailsPage(); int exptAnnotationsId = Integer.parseInt(portalHelper.getUrlParam("id")); return new DataFolderInfo(sourceFolder, targetFolder, shortAccessUrl, experimentTitle, exptAnnotationsId, submitter); @@ -169,7 +165,7 @@ private void testSendingReminders(String projectName, List dataF 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 experiments"); + verifyPipelineJobLogMessage(projectName, message, "Skipped posting reminders for 1 experiment"); // Change reminder frequency to 0 again. log("Changing reminder settings. Setting reminder frequency to 0."); @@ -183,7 +179,7 @@ private void testSendingReminders(String projectName, List dataF 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 experiments"); + 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()); @@ -277,14 +273,6 @@ private void verifyNoReminderPosted(String projectName, DataFolderInfo folderInf verifyReminderPosted(projectName, folderInfo, 0); } - private void verifyReminderPosted(String projectName, List folderInfos, int count) - { - for (DataFolderInfo folderInfo: folderInfos) - { - verifyReminderPosted(projectName, folderInfo, count); - } - } - private void verifyReminderPosted(String projectName, DataFolderInfo folderInfo, int count) { goToProjectFolder(projectName, folderInfo.getTargetFolder()); @@ -391,7 +379,7 @@ private void goToSendRemindersPage(String projectName) selectOptionByText(Locator.name("journal"), projectName); click(Locator.linkWithText("Send Reminders Now")); waitForText("Send Reminders"); - assertEquals("/" + projectName, getCurrentContainerPath()); + assertTextPresent(projectName, "A reminder message will be sent to the submitters of the selected experiments"); } private static class DataFolderInfo From 35569d2d8f760153bbdde805b073fe7c65321969 Mon Sep 17 00:00:00 2001 From: vagisha Date: Sun, 17 Aug 2025 09:16:35 -0700 Subject: [PATCH 13/18] Attempt to fix test --- .../test/tests/panoramapublic/PrivateDataReminderTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index 2e9e2960..6a6ac5c6 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -377,9 +377,8 @@ private void goToSendRemindersPage(String projectName) clickAndWait(Locator.linkWithText("Panorama Public")); clickAndWait(Locator.linkWithText("Private Data Reminder Settings")); selectOptionByText(Locator.name("journal"), projectName); - click(Locator.linkWithText("Send Reminders Now")); - waitForText("Send Reminders"); - assertTextPresent(projectName, "A reminder message will be sent to the submitters of the selected experiments"); + clickAndWait(Locator.linkWithText("Send Reminders Now")); + waitForText(projectName, "A reminder message will be sent to the submitters of the selected experiments"); } private static class DataFolderInfo From a862f39a55da02a157e2f8b93ad467d3b40d51d3 Mon Sep 17 00:00:00 2001 From: vagisha Date: Sun, 24 Aug 2025 17:09:40 -0700 Subject: [PATCH 14/18] CR feedback - Use table name constant and date format string constant. - isSortable and isFilterable return false for DatasetStatusColumn. - Added clarifying text for form input elements in privateDataRemindersSettingsForm.jsp --- .../panoramapublic/PanoramaPublicController.java | 10 ++++++---- .../panoramapublic/PanoramaPublicNotification.java | 2 +- .../message/PrivateDataReminderSettings.java | 2 +- .../pipeline/PrivateDataReminderJob.java | 3 ++- .../query/ExperimentAnnotationsTableInfo.java | 12 ++++++++++++ .../panoramapublic/view/createMessageForm.jsp | 2 +- .../view/privateDataRemindersSettingsForm.jsp | 13 +++++++++++++ .../view/sendPrivateDataRemindersForm.jsp | 2 +- .../panoramapublic/PrivateDataReminderTest.java | 2 +- 9 files changed, 38 insertions(+), 10 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 259adb9b..afb332a8 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -6316,7 +6316,7 @@ public void addNavTrail(NavTree root) List publishedVersions = ExperimentAnnotationsManager.getPublishedVersionsOfExperiment(sourceExperimentId); if (!publishedVersions.isEmpty()) { - QuerySettings qSettings = new QuerySettings(getViewContext(), "PublishedVersions", "ExperimentAnnotations"); + QuerySettings qSettings = new QuerySettings(getViewContext(), "PublishedVersions", PanoramaPublicSchema.TABLE_EXPERIMENT_ANNOTATIONS); qSettings.setBaseFilter(new SimpleFilter(new SimpleFilter(FieldKey.fromParts("SourceExperimentId"), sourceExperimentId))); List columns = new ArrayList<>(List.of(FieldKey.fromParts("Version"), FieldKey.fromParts("Created"), FieldKey.fromParts("Link"), FieldKey.fromParts("Share"))); @@ -9759,7 +9759,8 @@ public static class CreatePanoramaPublicMessageAction extends SimpleViewAction

keys) super.addQueryFieldKeys(keys); keys.add(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 index 740787e9..6635cd54 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp @@ -85,6 +85,11 @@

+
+ Number of months after data submission before sending the first reminder. +
+ Entering 0 will send a reminder the next time the job runs. +
+
+ Interval in months between reminder messages after the first one. +
+ Entering 0 will send a reminder the next time the job runs. +
+
+ Number of months the private status of a dataset can be extended at the submitter's request. +
diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp index 1161d874..5708b3c5 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/sendPrivateDataRemindersForm.jsp @@ -39,7 +39,7 @@ function submitForm() { const form = document.getElementById("send-reminders-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/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java index 6a6ac5c6..df358543 100644 --- a/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java +++ b/panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PrivateDataReminderTest.java @@ -323,7 +323,7 @@ private void postReminders(String projectName, boolean testMode, int expectedExp { goToSendRemindersPage(projectName); - DataRegionTable table = new DataRegionTable("ExperimentAnnotationsTable", getDriver()); + DataRegionTable table = new DataRegionTable("ExperimentAnnotations", getDriver()); assertEquals(expectedExperimentCount, table.getDataRowCount()); table.clearAllFilters(); From ffedecd023bda5b52e91ce65c1aa18bbe37fe9aa Mon Sep 17 00:00:00 2001 From: vagisha Date: Wed, 27 Aug 2025 12:07:33 -0700 Subject: [PATCH 15/18] - CR feedback: Removed ExceptionUtil.logExceptionToMothership - Added an option in the Private Date Reminder Setting form to enter a time for reminder jobs to run. --- .../PanoramaPublicController.java | 29 +++++++++-- .../message/PrivateDataMessageScheduler.java | 16 +++--- .../message/PrivateDataReminderSettings.java | 51 ++++++++++++++++++- .../view/privateDataRemindersSettingsForm.jsp | 17 +++++-- .../PrivateDataReminderTest.java | 6 +-- 5 files changed, 102 insertions(+), 17 deletions(-) diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index afb332a8..39f85fc8 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -10117,6 +10117,15 @@ 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 @@ -10126,6 +10135,7 @@ public ModelAndView getView(PrivateDataReminderSettingsForm form, boolean 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()); @@ -10143,6 +10153,7 @@ public boolean handlePost(PrivateDataReminderSettingsForm form, BindException er { 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()); @@ -10161,11 +10172,11 @@ public URLHelper getSuccessURL(PrivateDataReminderSettingsForm privateDataRemind @Override public ModelAndView getSuccessView(PrivateDataReminderSettingsForm form) { - ActionURL adminUrl = new ActionURL(PanoramaPublicAdminViewAction.class, getContainer()); + ActionURL url = new ActionURL(PrivateDataReminderSettingsAction.class, getContainer()); return new HtmlView( - DIV("Private data message settings saved!", + DIV("Private data reminder settings saved!", BR(), - new LinkBuilder("Back to Panorama Public Admin Console").href(adminUrl).build())); + new LinkBuilder("Back to Private Data Reminder Settings").href(url).build())); } @Override @@ -10179,6 +10190,7 @@ public void addNavTrail(NavTree root) public static class PrivateDataReminderSettingsForm { private boolean _enabled; + private String _reminderTime; private Integer _extensionLength; private Integer _reminderFrequency; private Integer _delayUntilFirstReminder; @@ -10193,6 +10205,16 @@ public void setEnabled(boolean enabled) _enabled = enabled; } + public String getReminderTime() + { + return _reminderTime; + } + + public void setReminderTime(String reminderTime) + { + _reminderTime = reminderTime; + } + public Integer getExtensionLength() { return _extensionLength; @@ -10248,6 +10270,7 @@ public ModelAndView getView(PrivateDataSendReminderForm form, boolean reshow, Bi 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()); diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java index 5c456190..90101344 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataMessageScheduler.java @@ -7,7 +7,6 @@ import org.labkey.api.pipeline.PipelineService; import org.labkey.api.security.User; import org.labkey.api.util.ConfigurationException; -import org.labkey.api.util.ExceptionUtil; import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.ViewBackgroundInfo; import org.labkey.panoramapublic.model.Journal; @@ -25,6 +24,8 @@ 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"); @@ -55,8 +56,9 @@ public void initialize(boolean enable) return; } + PrivateDataReminderSettings settings = PrivateDataReminderSettings.get(); // Get the quartz Trigger - Trigger trigger = getTrigger(); + 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) @@ -72,12 +74,14 @@ public void initialize(boolean enable) } } - protected Trigger getTrigger() + protected Trigger getTrigger(PrivateDataReminderSettings settings) { - // Run at 8:00AM every morning + LocalTime reminderTime = settings.getReminderTime(); + + // Runs every day at the specified time return TriggerBuilder.newTrigger() .withIdentity(TRIGGER_KEY) - .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(8, 0)) + .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(reminderTime.getHour(), reminderTime.getMinute())) .build(); } @@ -118,8 +122,6 @@ public void execute(JobExecutionContext context) catch(Exception e) { _log.error("Error queuing PrivateDataReminderJob", e); - ExceptionUtil.logExceptionToMothership(null, e); - } } } diff --git a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java index 366f952e..ee45663c 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java +++ b/panoramapublic/src/org/labkey/panoramapublic/message/PrivateDataReminderSettings.java @@ -9,26 +9,34 @@ 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; @@ -59,6 +67,9 @@ public static PrivateDataReminderSettings get() ? 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 { @@ -66,11 +77,33 @@ public static PrivateDataReminderSettings get() 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); @@ -78,6 +111,7 @@ public static void save(PrivateDataReminderSettings settings) 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(); } @@ -86,6 +120,11 @@ public void setEnableReminders(boolean enableReminders) _enableReminders = enableReminders; } + public void setReminderTime(LocalTime reminderTime) + { + _reminderTime = reminderTime; + } + public void setExtensionLength(int extensionLength) { _extensionLength = extensionLength; @@ -101,6 +140,16 @@ 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; @@ -178,7 +227,7 @@ private boolean isExtensionValidAsOf(@NotNull DatasetStatus status, @NotNull Dat return isDateInFuture(extensionValidUntil, currentTime); } - public boolean isLastReminderRecentAsOf(@NotNull DatasetStatus status, @NotNull Date currentTime) + private boolean isLastReminderRecentAsOf(@NotNull DatasetStatus status, @NotNull Date currentTime) { Date reminderValidUntil = getReminderValidUntilDate(status); return isDateInFuture(reminderValidUntil, currentTime); diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp index 6635cd54..af474fb9 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/privateDataRemindersSettingsForm.jsp @@ -79,13 +79,24 @@ />
+ <%=h(PrivateDataReminderSettings.PROP_REMINDER_TIME)%> + + +
+ Reminders will be sent daily at the specified time (e.g. <%=h(PrivateDataReminderSettings.DEFAULT_REMINDER_TIME)%>), if enabled. +
+
<%=h(PrivateDataReminderSettings.PROP_DELAY_UNTIL_FIRST_REMINDER)%> -
+
Number of months after data submission before sending the first reminder.
Entering 0 will send a reminder the next time the job runs. @@ -98,7 +109,7 @@
-
+
Interval in months between reminder messages after the first one.
Entering 0 will send a reminder the next time the job runs. @@ -111,7 +122,7 @@
-
+
Number of months the private status of a dataset can be extended at the submitter's request.