From 67067f9b87dd7022c5fb4197e6170f1ad2286344 Mon Sep 17 00:00:00 2001 From: vagisha Date: Fri, 3 May 2024 09:31:25 -0700 Subject: [PATCH] Add admin action to post to multiple Panorama Public submission request message threads (#430) - Added an admin console action that will allow site admins to post to multiple Panorama Public submission request message threads. Show link to post messages on the admin page only if a "Panorama Public" journal project exists on the server. - Added a form where site admins can enter a title and message (in Markdown syntax), and filter and select from the list of datasets on Panorama Public where the message should be posted. -- Display an example message on the confirmation page. -- Added placeholder texts for links in message - Added a pipeline job to post the message to the selected experiments' message threads. -- Add submitter and lab head to the notify list if they were not on the original message thread posted before we switched to secure messages for Panorama Public. - "Support Message" column was added to the ExperimentAnnotationsTableInfo so that site admins can easily navigate to the message thread for a dataset - Removed old, unused admin action AssignSubmitterPermissionAction and related pipeline job.. - Added experiment title and permanent link on the form to make data public. --- .../PanoramaPublicController.java | 316 ++++++++++++++---- .../panoramapublic/PanoramaPublicModule.java | 1 - .../PanoramaPublicNotification.java | 82 ++++- .../AssignSubmitterPermissionJob.java | 148 -------- .../PostPanoramaPublicMessageJob.java | 175 ++++++++++ .../query/ExperimentAnnotationsTableInfo.java | 67 ++++ .../panoramapublic/query/JournalManager.java | 1 + .../view/confirmPostMessage.jsp | 61 ++++ .../panoramapublic/view/createMessageForm.jsp | 99 ++++++ .../view/publish/publicationDetails.jsp | 7 + 10 files changed, 745 insertions(+), 212 deletions(-) delete mode 100644 panoramapublic/src/org/labkey/panoramapublic/pipeline/AssignSubmitterPermissionJob.java create mode 100644 panoramapublic/src/org/labkey/panoramapublic/pipeline/PostPanoramaPublicMessageJob.java create mode 100644 panoramapublic/src/org/labkey/panoramapublic/view/confirmPostMessage.jsp create mode 100644 panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 4e8dcf0d..b295e817 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java @@ -38,6 +38,8 @@ import org.labkey.api.action.SimpleStreamAction; import org.labkey.api.action.SimpleViewAction; import org.labkey.api.action.SpringActionController; +import org.labkey.api.announcements.api.Announcement; +import org.labkey.api.announcements.api.AnnouncementService; import org.labkey.api.attachments.Attachment; import org.labkey.api.attachments.AttachmentFile; import org.labkey.api.attachments.AttachmentForm; @@ -132,6 +134,8 @@ import org.labkey.api.util.logging.LogHelper; import org.labkey.api.view.*; import org.labkey.api.view.template.ClientDependency; +import org.labkey.api.wiki.WikiRendererType; +import org.labkey.api.wiki.WikiRenderingService; import org.labkey.panoramapublic.catalog.CatalogEntrySettings; import org.labkey.panoramapublic.catalog.CatalogImageAttachmentParent; import org.labkey.panoramapublic.chromlib.ChromLibStateManager; @@ -154,8 +158,8 @@ import org.labkey.panoramapublic.model.validation.DataValidation; import org.labkey.panoramapublic.model.validation.PxStatus; import org.labkey.panoramapublic.model.validation.Status; -import org.labkey.panoramapublic.pipeline.AssignSubmitterPermissionJob; import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineJob; +import org.labkey.panoramapublic.pipeline.PostPanoramaPublicMessageJob; import org.labkey.panoramapublic.pipeline.PxDataValidationPipelineJob; import org.labkey.panoramapublic.pipeline.PxValidationPipelineProvider; import org.labkey.panoramapublic.proteomexchange.ChemElement; @@ -238,7 +242,6 @@ import static org.labkey.api.util.DOM.Attribute.type; 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.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; @@ -298,7 +301,7 @@ public ModelAndView getView(Object o, BindException errors) view.addView(getPXCredentialsLink()); view.addView(getDataCiteCredentialsLink()); view.addView(getPanoramaPublicCatalogSettingsLink()); - view.addView(getAssignSubmitterPermissionsLink()); + view.addView(getPostSupportMessageLink()); view.setFrame(WebPartView.FrameType.PORTAL); view.setTitle("Panorama Public Settings"); return view; @@ -324,12 +327,17 @@ private ModelAndView getPanoramaPublicCatalogSettingsLink() return new HtmlView(DIV(at(style, "margin-top:20px;"), new Link.LinkBuilder("Panorama Public Catalog Settings").href(url).build())); } - - private ModelAndView getAssignSubmitterPermissionsLink() + + private ModelAndView getPostSupportMessageLink() { - ActionURL url = new ActionURL(AssignSubmitterPermissionAction.class, getContainer()); - return new HtmlView(DIV(at(style, "margin-top:20px;"), - new Link.LinkBuilder("Assign PanoramaPublicSubmitterRole").href(url).build())); + Journal panoramaPublic = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); + if (panoramaPublic != null) + { + ActionURL url = new ActionURL(CreatePanoramaPublicMessageAction.class, panoramaPublic.getProject()); + return new HtmlView(DIV(at(style, "margin-top:20px;"), + new Link.LinkBuilder("Post to Panorama Public Support Messages").href(url).build())); + } + return null; } @Override @@ -6674,6 +6682,8 @@ public static class PublicationDetailsBean private final boolean _isPeerReviewed; private final DataLicense _license; + private final String _experimentTitle; + public PublicationDetailsBean(PublicationDetailsForm form, ExperimentAnnotations copiedExperiment) { _form = form; @@ -6681,6 +6691,7 @@ public PublicationDetailsBean(PublicationDetailsForm form, ExperimentAnnotations _isPeerReviewed = copiedExperiment.isPeerReviewed(); _accessUrl = copiedExperiment.getShortUrl().renderShortURL(); _license = copiedExperiment.getDataLicense(); + _experimentTitle = copiedExperiment.getTitle(); } public PublicationDetailsForm getForm() @@ -6707,6 +6718,11 @@ public DataLicense getLicense() { return _license; } + + public String getExperimentTitle() + { + return _experimentTitle; + } } public static class PublishSuccessViewBean @@ -9212,100 +9228,280 @@ public ModelAndView getView(Object o, BindException errors) throws Exception } } - // ------------------------------------------------------------------------ - // BEGIN Assign PanoramaPublicSubmitterRole to data submitters and lab heads - // ------------------------------------------------------------------------ @RequiresSiteAdmin - public class AssignSubmitterPermissionAction extends FormViewAction + public class CreatePanoramaPublicMessageAction extends SimpleViewAction { @Override - public void validateCommand(AssignSubmitterPermissionForm target, Errors errors) {} + public ModelAndView getView(PanoramaPublicMessageForm form, BindException errors) throws Exception + { + QuerySettings qSettings = new QuerySettings(getViewContext(), "ExperimentAnnotationsTable", "ExperimentAnnotations"); + qSettings.setContainerFilterName(ContainerFilter.Type.CurrentAndSubfolders.name()); + QueryView tableView = new QueryView(new PanoramaPublicSchema(getUser(), getContainer()), qSettings, errors); + tableView.setTitle("Panorama Public Experiments"); + + form.setDataRegionName(tableView.getDataRegionName()); + + JspView jspView = new JspView<>("/org/labkey/panoramapublic/view/createMessageForm.jsp", form, errors); + return new VBox(jspView, tableView); + } @Override - public ModelAndView getView(AssignSubmitterPermissionForm form, boolean reshow, BindException errors) + public void addNavTrail(NavTree root) { - return new HtmlView("Assign PanoramaPublicSubmitterRole", - DIV(LK.ERRORS(errors), - "Assign PanoramaPublicSubmitterRole to data submitters and lab heads", - FORM( - at(method, "POST"), - TABLE( - TR(TD(at(style, "padding-right:10px;"), LABEL("Dry Run: ")), - TD(CHECKBOX(at(name, "dryRun", checked, form.isDryRun())))), - TR(TD(at(style, "padding-right:10px;"), LABEL("Project: ")), - TD(INPUT(at(name, "project", value, form.getProject())))) - ), - DIV(at(style, "margin-top:10px"), - new Button.ButtonBuilder("Start").submit(true).style("margin-right:10px").build(), - new Button.ButtonBuilder("Cancel").href(new ActionURL(PanoramaPublicAdminViewAction.class, getContainer())).build() - ) - ) - )); + root.addChild("Message for Panorama Public Submitters"); } + } + @RequiresSiteAdmin + public class PostPanoramaPublicMessageAction extends ConfirmAction + { @Override - public boolean handlePost(AssignSubmitterPermissionForm form, BindException errors) throws Exception + public void validateCommand(PanoramaPublicMessageForm form, Errors errors) { - if (StringUtils.isEmpty(form.getProject())) + List selectedExperimentIds = form.getSelectedExperimentIds(); + if (selectedExperimentIds.isEmpty()) { - errors.reject(ERROR_MSG, "Please enter the name of a project"); - return false; + errors.reject(ERROR_MSG, "Please select at least one experiment"); } - Container container = ContainerManager.getForPath(form.getProject()); - if (container == null) + if (StringUtils.isEmpty(form.getMessageTitle())) { - errors.reject(ERROR_MSG, "Cannot find project " + form.getProject()); - return false; + errors.reject(ERROR_MSG, "Please enter a prefix for the message title"); } + if (StringUtils.isEmpty(form.getMessage())) + { + errors.reject(ERROR_MSG, "Please enter a message"); + } + else + { + List validationErrors = new ArrayList<>(); - PipelineJob job = new AssignSubmitterPermissionJob(getViewBackgroundInfo(), PipelineService.get().getPipelineRootSetting(ContainerManager.getRoot()), - form.isDryRun(), container); + HtmlString formattedMessage = WikiRenderingService.get().getFormattedHtml(WikiRendererType.MARKDOWN, form.getMessage()); + PageFlowUtil.validateHtml(formattedMessage.renderToString(), validationErrors, false); + if (!validationErrors.isEmpty()) + { + errors.reject(ERROR_MSG, "Message could not be validated due to the following errors:"); + for (String err : validationErrors) + { + errors.reject(ERROR_MSG, err); + } + } + } + } + + @Override + public ModelAndView getConfirmView(PanoramaPublicMessageForm form, BindException errors) + { + List selectedExperimentIds = form.getSelectedExperimentIds(); + List experiments = new ArrayList<>(); + for (Integer selectedId: selectedExperimentIds) + { + experiments.add(ExperimentAnnotationsManager.get(selectedId)); + } + form.setExperiments(experiments); + + MessageExampleBean exampleBean = createExampleMessage(selectedExperimentIds, form, errors); + if (exampleBean == null) + { + errors.reject(ERROR_MSG, "Could not create an example message from the selected experiment Ids"); + return new SimpleErrorView(errors, true); + } + + return new JspView<>("/org/labkey/panoramapublic/view/confirmPostMessage.jsp", exampleBean, errors); + } + + private MessageExampleBean createExampleMessage(List experimentIds, PanoramaPublicMessageForm form, BindException errors) + { + Journal journal = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); + Container announcementsContainer = journal.getSupportContainer(); + AnnouncementService announcementSvc = AnnouncementService.get(); + + for (Integer experimentId: experimentIds) + { + ExperimentAnnotations expAnnotations = ExperimentAnnotationsManager.get(experimentId); + JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(expAnnotations); + if (submission == null || submission.getLatestSubmission() == null) + { + continue; + } + Announcement announcement = announcementSvc.getAnnouncement(announcementsContainer, getUser(), submission.getAnnouncementId()); + if (announcement == null) + { + continue; // old data before we started posting submission requests to a message board + } + + String title = !StringUtils.isBlank(form.getMessageTitle()) ? StringUtils.trim(form.getMessageTitle()) + " " + expAnnotations.getShortUrl().renderShortURL() : announcement.getTitle(); + String message = PanoramaPublicNotification.replaceLinkPlaceholders(form.getMessage(), expAnnotations, announcement, announcementsContainer); + StringBuilder messageBody = PanoramaPublicNotification.getFullMessageBody(message, expAnnotations.getSubmitterUser(), getUser()); + if (messageBody.toString().contains(PanoramaPublicNotification.PLACEHOLDER)) + { + errors.reject(ERROR_MSG, "Some placeholders were not substituted"); + } + MessageExampleBean example = new MessageExampleBean(); + example.setForm(form); + example.setExperimentAnnotations(expAnnotations); + example.setTitle(title); + example.setMessage(messageBody.toString()); + example.setMarkdownMessage(WikiRenderingService.get().getFormattedHtml(WikiRendererType.MARKDOWN, messageBody.toString())); + return example; + } + return null; + } + + @Override + public boolean handlePost(PanoramaPublicMessageForm form, BindException errors) throws Exception + { + List selectedExperimentIds = form.getSelectedExperimentIds(); + PipelineJob job = new PostPanoramaPublicMessageJob(getViewBackgroundInfo(), PipelineService.get().getPipelineRootSetting(ContainerManager.getRoot()), + selectedExperimentIds, form.getMessage(), form.getMessageTitle(), form.getTestMode()); PipelineService.get().queueJob(job); return true; } @Override - public URLHelper getSuccessURL(AssignSubmitterPermissionForm form) + public URLHelper getSuccessURL(PanoramaPublicMessageForm form) { return PageFlowUtil.urlProvider(PipelineUrls.class).urlBegin(getContainer()); } + } - @Override - public void addNavTrail(NavTree root) + public static class MessageExampleBean + { + private ExperimentAnnotations _experimentAnnotations; + private String _title; + private HtmlString _markdownMessage; + private String _message; + + private PanoramaPublicMessageForm _form; + + public ExperimentAnnotations getExperimentAnnotations() { - addPanoramaPublicAdminConsoleNav(root, getContainer()); - root.addChild("Assign PanoramaPublicSubmitterRole"); + return _experimentAnnotations; + } + + public void setExperimentAnnotations(ExperimentAnnotations experimentAnnotations) + { + _experimentAnnotations = experimentAnnotations; + } + + public String getTitle() + { + return _title; + } + + public void setTitle(String title) + { + _title = title; + } + + public HtmlString getMarkdownMessage() + { + return _markdownMessage; + } + + public void setMarkdownMessage(HtmlString markdownMessage) + { + _markdownMessage = markdownMessage; + } + + public String getMessage() + { + return _message; + } + + public void setMessage(String message) + { + _message = message; + } + + public PanoramaPublicMessageForm getForm() + { + return _form; + } + + public void setForm(PanoramaPublicMessageForm form) + { + _form = form; } } - private static class AssignSubmitterPermissionForm + public static class PanoramaPublicMessageForm { - private boolean _dryRun; - private String _project; + private String _messageTitle = null; + private String _message; + private boolean _testMode; + private String _selectedIds; + private String _dataRegionName = null; + private List _experiments = null; - public boolean isDryRun() + public String getMessageTitle() { - return _dryRun; + return _messageTitle; } - public void setDryRun(boolean dryRun) + public void setMessageTitle(String messageTitle) { - _dryRun = dryRun; + _messageTitle = messageTitle; } - public String getProject() + public String getMessage() { - return _project; + return _message; } - public void setProject(String project) + public void setMessage(String message) { - _project = project; + _message = message; + } + + public boolean getTestMode() + { + return _testMode; + } + + public void setTestMode(boolean testMode) + { + _testMode = testMode; + } + + public String getSelectedIds() + { + return _selectedIds; + } + + public List getSelectedExperimentIds() + { + if (_selectedIds == null) + { + return Collections.emptyList(); + } + return Arrays.stream(StringUtils.split(_selectedIds, ",")).map(Integer::parseInt).collect(Collectors.toList()); + } + + public void setSelectedIds(String selectedIds) + { + _selectedIds = selectedIds; + } + + public String getDataRegionName() + { + return _dataRegionName; + } + + public void setDataRegionName(String dataRegionName) + { + _dataRegionName = dataRegionName; + } + + public List getExperiments() + { + return _experiments; + } + + public void setExperiments(List experiments) + { + _experiments = experiments; } } - // ------------------------------------------------------------------------ - // END Assign PanoramaPublicSubmitterRole to data submitters and lab heads - // ------------------------------------------------------------------------ public static ActionURL getEditExperimentDetailsURL(Container c, int experimentAnnotationsId, URLHelper returnURL) { diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java index c6d3b13b..6bb05523 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java @@ -45,7 +45,6 @@ import org.labkey.panoramapublic.catalog.CatalogImageAttachmentType; import org.labkey.panoramapublic.model.Journal; import org.labkey.panoramapublic.model.speclib.SpecLibKey; -import org.labkey.panoramapublic.pipeline.CopyExperimentFinalTask; import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineProvider; import org.labkey.panoramapublic.pipeline.PxValidationPipelineProvider; import org.labkey.panoramapublic.proteomexchange.ExperimentModificationGetter; diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java index f87f5f40..ab9bb47d 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java +++ b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java @@ -227,12 +227,11 @@ private static void postNotification(ExperimentAnnotations experimentAnnotations postNotification(journal, je, messageBody.toString(), journalAdmin, messageTitlePrefix, status, getNotifyList(experimentAnnotations, user)); } - private static void postNotification(Journal journal, JournalExperiment je, String messageBody, User messagePoster, - @NotNull String messageTitlePrefix, @NotNull StatusOption status, @Nullable List notifyUsers) + private static void postNotificationFullTitle(Journal journal, JournalExperiment je, String messageBody, User messagePoster, + @NotNull String messageTitle, @NotNull StatusOption status, @Nullable List notifyUsers) { AnnouncementService svc = AnnouncementService.get(); Container supportContainer = journal.getSupportContainer(); - String messageTitle = messageTitlePrefix +" - " + je.getShortAccessUrl().renderShortURL(); Set combinedNotifyUserIds = new HashSet<>(); if (notifyUsers != null) @@ -264,6 +263,83 @@ private static void postNotification(Journal journal, JournalExperiment je, Stri SubmissionManager.updateJournalExperiment(je, messagePoster); } } + private static void postNotification(Journal journal, JournalExperiment je, String messageBody, User messagePoster, + @NotNull String messageTitlePrefix, @NotNull StatusOption status, @Nullable List notifyUsers) + { + String messageTitle = messageTitlePrefix +" - " + je.getShortAccessUrl().renderShortURL(); + postNotificationFullTitle(journal, je, messageBody, messagePoster, messageTitle, status, notifyUsers); + } + + public static void postNotification(Journal journal, JournalExperiment je, String text, User messageTo, User messagePoster, + @NotNull String messageTitle, @NotNull StatusOption status, @Nullable List notifyUsers) + { + StringBuilder messageBody = getFullMessageBody(text, messageTo, messagePoster); + postNotificationFullTitle(journal, je, messageBody.toString(), messagePoster, messageTitle, status, notifyUsers); + } + + @NotNull + public static StringBuilder getFullMessageBody(String text, User messageTo, User messagePoster) + { + StringBuilder messageBody = new StringBuilder(); + messageBody.append("Dear ").append(getUserName(messageTo)).append(",").append(NL2); + messageBody.append(text); + messageBody.append(NL2).append("Best regards,"); + messageBody.append(NL).append(getUserName(messagePoster)); + return messageBody; + } + + // The following link placeholders can be used in messages posted through the Panorama Public admin console (PostPanoramaPublicMessageAction). + // An example message (Markdown format): + /* + We have updated the password policy on PanoramaWeb. If you have not yet updated the password for the reviewer account assigned to + this data (available at __PH__DATA__SHORT__URL__), we ask that you do so as soon as possible. + + **How to Change the Password**: Log in with the reviewer account's email and current password, and follow the prompt to set a new password. + For details on the reviewer account associated with your data, please refer to the message that was sent when this data was copied to + Panorama Public or view the full message thread at this link: [Message Thread](__PH__MESSAGE__THREAD__URL__) + + **For Published Data**: + If your data is already published and you no longer need the reviewer account, we encourage you to make the data public. This can be + easily done by clicking the "Make Public" button in your data folder or by clicking this link: [Make Data Public](__PH__MAKE__DATA__PUBLIC__URL__) + + We apologize for any inconvenience this update may cause. We are here to assist you if you have any questions or need help updating your password. + Please respond to this message by [**clicking here**](__PH__RESPOND__TO__MESSAGE__URL__) for further clarification or support. + */ + public static String PLACEHOLDER = "__PH__"; + public static String PLACEHOLDER_MESSAGE_THREAD_URL = PLACEHOLDER + "MESSAGE__THREAD__URL__"; + 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) + { + String toReturn = text; + if (toReturn.contains(PLACEHOLDER_SHORT_URL)) + { + toReturn = toReturn.replaceAll(PLACEHOLDER_SHORT_URL, expAnnotations.getShortUrl().renderShortURL()); + } + if (toReturn.contains(PLACEHOLDER_MESSAGE_THREAD_URL)) + { + ActionURL viewMessageUrl = new ActionURL("announcements", "thread", announcementContainer) + .addParameter("rowId", announcement.getRowId()); + toReturn = toReturn.replaceAll(PLACEHOLDER_MESSAGE_THREAD_URL, viewMessageUrl.getLocalURIString()); + } + if (toReturn.contains(PLACEHOLDER_RESPOND_TO_MESSAGE_URL)) + { + ActionURL viewMessageUrl = new ActionURL("announcements", "thread", announcementContainer) + .addParameter("rowId", announcement.getRowId()); + ActionURL respondToMessageUrl = new ActionURL("announcements", "respond", announcementContainer) + .addParameter("parentId", announcement.getEntityId()) + .addReturnURL(viewMessageUrl); + toReturn = toReturn.replaceAll(PLACEHOLDER_RESPOND_TO_MESSAGE_URL, respondToMessageUrl.getLocalURIString()); + } + if (toReturn.contains(PLACEHOLDER_MAKE_DATA_PUBLIC_URL)) + { + ActionURL makePublicUrl = PanoramaPublicController.getMakePublicUrl(expAnnotations.getId(), expAnnotations.getContainer()); + toReturn = toReturn.replaceAll(PLACEHOLDER_MAKE_DATA_PUBLIC_URL, makePublicUrl.getLocalURIString()); + } + return toReturn; + } private static void appendSubmissionDetails(ExperimentAnnotations expAnnotations, Journal journal, JournalExperiment je, Submission submission, @Nullable ExperimentAnnotations currentJournalExpt, StringBuilder text) diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/AssignSubmitterPermissionJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/AssignSubmitterPermissionJob.java deleted file mode 100644 index 40fc8145..00000000 --- a/panoramapublic/src/org/labkey/panoramapublic/pipeline/AssignSubmitterPermissionJob.java +++ /dev/null @@ -1,148 +0,0 @@ -package org.labkey.panoramapublic.pipeline; - -import org.apache.logging.log4j.Logger; -import org.jetbrains.annotations.NotNull; -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.MutableSecurityPolicy; -import org.labkey.api.security.SecurityPolicyManager; -import org.labkey.api.security.User; -import org.labkey.api.security.UserManager; -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.model.ExperimentAnnotations; -import org.labkey.panoramapublic.model.JournalSubmission; -import org.labkey.panoramapublic.query.ExperimentAnnotationsManager; -import org.labkey.panoramapublic.query.SubmissionManager; -import org.labkey.panoramapublic.security.PanoramaPublicSubmitterPermission; -import org.labkey.panoramapublic.security.PanoramaPublicSubmitterRole; - -public class AssignSubmitterPermissionJob extends PipelineJob -{ - private boolean _dryRun; - private Container _project; - - // For serialization - protected AssignSubmitterPermissionJob() - { - } - - public AssignSubmitterPermissionJob(ViewBackgroundInfo info, @NotNull PipeRoot root, boolean dryRun, Container project) - { - super("Panorama Public", info, root); - setLogFile(root.getRootNioPath().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-assign-submitter-role", "log"))); - _dryRun = dryRun; - _project = project; - } - - @Override - public void run() - { - setStatus(PipelineJob.TaskStatus.running); - if (_project != null) - { - assignRole(); - setStatus(PipelineJob.TaskStatus.complete); - } - else - { - getLogger().error("Input project was null. Exiting..."); - } - } - - private void assignRole() - { - var containers = ContainerManager.getAllChildren(_project, getUser()); - getLogger().info("Total number of folders: " + containers.size()); - - int done = 0; - int updated = 0; - int notExptFolders = 0; - int total = containers.size(); - - try (DbScope.Transaction transaction = PanoramaPublicManager.getSchema().getScope().ensureTransaction()) - { - for (Container container : containers) - { - ExperimentAnnotations expAnnotations = ExperimentAnnotationsManager.getExperimentInContainer(container); - if (expAnnotations != null && expAnnotations.isJournalCopy()) - { - boolean submitterUpdated = assignRole(expAnnotations.getSubmitterUser(), "Submitter", container, _dryRun, getLogger()); - boolean labHeadUpdated = assignRole(expAnnotations.getLabHeadUser(), "Lab Head", container, _dryRun, getLogger()); - - JournalSubmission submission = SubmissionManager.getSubmissionForExperiment(expAnnotations); - boolean formSubmitterUpdated = false; - if (submission != null) - { - // Assign the role to the user that submitted the request. This may not be the same as the user selected as the "Submitter". - User formSubmitter = UserManager.getUser(submission.getJournalExperiment().getCreatedBy()); - assignRole(formSubmitter, "Form submitter", container, _dryRun, getLogger()); - } - - if (submitterUpdated || labHeadUpdated || formSubmitterUpdated) - { - updated++; - } - } - else - { - getLogger().info(String.format("'%s' - no valid experiment", container.getPath())); - notExptFolders++; - } - - done++; - if (done % 100 == 0) - { - getLogger().info(done + "/" + total + " done."); - } - } - transaction.commit(); - } - - getLogger().info("All folders: " + total + "; Folders with valid experiments: " + (total - notExptFolders)); - getLogger().info("Assigned PanoramaPublicSubmitterRole in " + updated + " folders."); - } - - private boolean assignRole(User user, String userType, Container container, boolean dryRun, Logger logger) - { - if (user == null) return false; - - if (!container.hasPermission(user, PanoramaPublicSubmitterPermission.class)) - { - if (!dryRun) - { - logger.info(String.format("'%s', %s: %s - %s", container.getPath(), userType, user.getEmail(), "assigning")); - MutableSecurityPolicy newPolicy = new MutableSecurityPolicy(container, container.getPolicy()); - newPolicy.addRoleAssignment(user, PanoramaPublicSubmitterRole.class, false); - SecurityPolicyManager.savePolicy(newPolicy, User.getAdminServiceUser()); - return true; - } - else - { - logger.info(String.format("'%s', %s: %s - %s", container.getPath(), userType, user.getEmail(), "would assign (dry run)")); - } - } - else - { - logger.info(String.format("'%s', %s: %s - %s", container.getPath(), userType, user.getEmail(), "already assigned")); - } - return false; - } - - @Override - public URLHelper getStatusHref() - { - return null; - } - - @Override - public String getDescription() - { - return "Assign PanoramaPublicSubmitterRole to data submitters and lab heads" + (_dryRun ? " (dry run)" : ""); - } -} diff --git a/panoramapublic/src/org/labkey/panoramapublic/pipeline/PostPanoramaPublicMessageJob.java b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PostPanoramaPublicMessageJob.java new file mode 100644 index 00000000..f6f0e3a4 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/pipeline/PostPanoramaPublicMessageJob.java @@ -0,0 +1,175 @@ +package org.labkey.panoramapublic.pipeline; + +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.labkey.api.announcements.DiscussionService; +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.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.ExperimentAnnotations; +import org.labkey.panoramapublic.model.Journal; +import org.labkey.panoramapublic.model.JournalSubmission; +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 PostPanoramaPublicMessageJob extends PipelineJob +{ + private String _titlePrefix; + private String _message; + private List _experimentAnnotationsIds; + private boolean _test; + + protected PostPanoramaPublicMessageJob() + { + } + + public PostPanoramaPublicMessageJob(ViewBackgroundInfo info, @NotNull PipeRoot root, List experimentAnnotationsIds, String message, + String titlePrefix, boolean test) + { + super("Panorama Public", info, root); + setLogFile(root.getRootNioPath().resolve(FileUtil.makeFileNameWithTimestamp("PanoramaPublic-post-to-message-thread", "log"))); + _experimentAnnotationsIds = experimentAnnotationsIds; + _message = message; + _titlePrefix = titlePrefix; + _test = test; + } + + @Override + public void run() + { + setStatus(PipelineJob.TaskStatus.running); + if (StringUtils.isEmpty(_message)) + { + getLogger().error("Message was blank. Exiting..."); + return; + } + if (_experimentAnnotationsIds == null || _experimentAnnotationsIds.isEmpty()) + { + getLogger().error("No experiment Ids were found. Exiting..."); + return; + } + postMessage(); + setStatus(PipelineJob.TaskStatus.complete); + + } + + private void postMessage() + { + getLogger().info(String.format("%sPosting to: %d message threads", _test ? "TEST MODE: " : "", _experimentAnnotationsIds.size())); + + int done = 0; + + AnnouncementService announcementSvc = AnnouncementService.get(); + Journal journal = JournalManager.getJournal(JournalManager.PANORAMA_PUBLIC); + Container announcementsContainer = journal.getSupportContainer(); + + List experimentNotFound = new ArrayList<>(); + List submissionNotFound = new ArrayList<>(); + List announcementNotFound = new ArrayList<>(); + List submitterNotFound = new ArrayList<>(); + + Set exptIds = new HashSet<>(_experimentAnnotationsIds); + 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(announcementsContainer, 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 " + announcementsContainer.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; + } + String title = !StringUtils.isBlank(_titlePrefix) ? StringUtils.trim(_titlePrefix) + " " + expAnnotations.getShortUrl().renderShortURL() : announcement.getTitle(); + if (!_test) + { + String placeholdersSubstituted = PanoramaPublicNotification.replaceLinkPlaceholders(_message, expAnnotations, announcement, announcementsContainer); + + // 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.postNotification(journal, submission.getJournalExperiment(), placeholdersSubstituted, submitter, getUser(), + title, DiscussionService.StatusOption.Closed, 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 selected experiments"; + } +} diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java index 764d589d..16118b46 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/ExperimentAnnotationsTableInfo.java @@ -38,6 +38,7 @@ import org.labkey.api.query.ExprColumn; import org.labkey.api.query.FieldKey; import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.QueryForeignKey; import org.labkey.api.query.UserIdQueryForeignKey; import org.labkey.api.query.UserIdRenderer; import org.labkey.api.query.UserSchema; @@ -70,6 +71,7 @@ import org.labkey.panoramapublic.model.CatalogEntry; import org.labkey.panoramapublic.model.DataLicense; import org.labkey.panoramapublic.model.ExperimentAnnotations; +import org.labkey.panoramapublic.model.Journal; import org.labkey.panoramapublic.view.publish.CatalogEntryWebPart; import org.labkey.panoramapublic.view.publish.ShortUrlDisplayColumnFactory; @@ -240,6 +242,12 @@ public void renderGridCellContents(RenderContext ctx, Writer out) throws IOExcep var isPublicCol = getIsPublicCol(); addColumn(isPublicCol); + if (schema.getUser().hasSiteAdminPermission()) + { + // For site admins, add a column linking to the message thread for the Panorama Public submission request. + addColumn(getAnnouncementIdCol()); + } + var licenseCol = wrapColumn("Data License", getRealTable().getColumn("Id")); licenseCol.setURLTargetWindow("_blank"); licenseCol.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo) @@ -399,6 +407,65 @@ private ExprColumn getIsPublicCol() return isPublicCol; } + private ExprColumn getAnnouncementIdCol() + { + SQLFragment sqlFragment = new SQLFragment(" (SELECT CASE WHEN ").append(ExprColumn.STR_TABLE_ALIAS + ".sourceexperimentid IS NOT NULL") + .append(" THEN ( SELECT announcementId from ").append(PanoramaPublicManager.getTableInfoJournalExperiment(), "je") + .append(" INNER JOIN ").append(PanoramaPublicManager.getTableInfoSubmission(), "s") + .append(" ON je.Id = s.journalExperimentId WHERE s.copiedExperimentId = ").append(ExprColumn.STR_TABLE_ALIAS + ".Id )") + .append(" ELSE (SELECT announcementId FROM ").append(PanoramaPublicManager.getTableInfoJournalExperiment(), "je") + .append(" WHERE je.experimentAnnotationsId = ").append(ExprColumn.STR_TABLE_ALIAS + ".Id )") + .append(" END ) "); + + var announcementCol = new ExprColumn(this, "SupportMessage", sqlFragment, JdbcType.INTEGER); + + // Add a FK lookup so that we can access the "Modified" etc. columns on the "Announcement" table. + announcementCol.setFk(QueryForeignKey.from(getUserSchema(), + ContainerFilter.EVERYTHING) // Announcements are not in the same container. e.g. on PanoramaWeb they are in "/home/support/panorama public requests" + .schema("announcement") // Cannot use CommSchemma.getSchemaName() which returns "comm". This only works if we use "announcement". + // AnnouncementSchema is not part of the LabKey API. + .to("Announcement", // Table name cannot be the plural, "Announcements", returned by CommSchema.getInstance().getTableInfoAnnouncements().getName() + "RowId", null)); + + announcementCol.setDisplayColumnFactory(new DisplayColumnFactory() + { + @Override + public DisplayColumn createRenderer(ColumnInfo colInfo) + { + return new DataColumn(colInfo) + { + @Override + public String renderURL(RenderContext ctx) + { + Integer announcementId = ctx.get(colInfo.getFieldKey(), Integer.class); + Integer experimentAnnotationsId = ctx.get(FieldKey.fromParts("Id"), Integer.class); + if (announcementId != null && experimentAnnotationsId != null) + { + Integer submittedExperimentId = ctx.get(FieldKey.fromParts("SourceExperimentId"), Integer.class); + List journals = JournalManager.getJournalsForExperiment(submittedExperimentId != null ? submittedExperimentId : experimentAnnotationsId); + if (!journals.isEmpty()) + { + ActionURL url = new ActionURL("announcements", "thread", journals.get(0).getSupportContainer()) + .addParameter("rowId", announcementId); + return url.getEncodedLocalURIString(); + } + } + return super.renderURL(ctx); + } + + @Override + public void addQueryFieldKeys(Set keys) + { + super.addQueryFieldKeys(keys); + keys.add(FieldKey.fromParts("Id")); + keys.add(FieldKey.fromParts("SourceExperimentId")); + } + }; + } + }); + return announcementCol; + } + @Override public String getName() { diff --git a/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java b/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java index a8fd7fbb..86a37ec7 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java +++ b/panoramapublic/src/org/labkey/panoramapublic/query/JournalManager.java @@ -66,6 +66,7 @@ */ public class JournalManager { + public static final String PANORAMA_PUBLIC = "Panorama Public"; private static final String PUBLIC_DATA_USER = "Public Data User"; private static final String USER_ID = "User Id"; private static final String USER_PASSWORD = "User Password"; diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/confirmPostMessage.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/confirmPostMessage.jsp new file mode 100644 index 00000000..a2bf8a28 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/view/confirmPostMessage.jsp @@ -0,0 +1,61 @@ +<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> +<%@ page import="org.labkey.panoramapublic.PanoramaPublicController" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.panoramapublic.model.ExperimentAnnotations" %> +<%@ page import="org.labkey.api.util.StringUtilsLabKey" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<% + JspView view = (JspView) HttpView.currentView(); + var messageExample = view.getModelBean(); + var exampleExpAnnot = messageExample.getExperimentAnnotations(); + var exampleShortUrl = exampleExpAnnot.getShortUrl().renderShortURL(); + var form = messageExample.getForm(); +%> + +
+ This following message will be posted to the support message threads of the selected experiments. + The message below is an example for experiment Id <%=exampleExpAnnot.getId()%> (<%=link(exampleShortUrl, exampleShortUrl).clearClasses()%>). +
+ + + + + + + + + + + + + + + + + +
Test Mode:<%=h(form.getTestMode() ? "Yes" : "No (message will be posted)")%>
Message Title:<%=h(messageExample.getTitle())%>
Message (Markdown):<%=messageExample.getMarkdownMessage()%>
Message:<%=h(messageExample.getMessage())%>
+
+ The message will be posted to the support message threads of the following <%=h(StringUtilsLabKey.pluralize(form.getExperiments().size(), "experiment"))%> + submitted to Panorama Public. + + + + + + + + + <% String trClass = "labkey-alternate-row"; + for (ExperimentAnnotations experiment: form.getExperiments()) { + var shortUrl = experiment.getShortUrl().renderShortURL(); + %> + + + + + + + <% trClass = "labkey-alternate-row".equals(trClass) ? "labkey-row" : "labkey-alternate-row"; } %> +
ExperimentIdCreatedShort URLTitle
<%=h(experiment.getId())%><%=h(experiment.getCreated())%><%=link(shortUrl, shortUrl)%><%=h(experiment.getTitle())%>
+
\ No newline at end of file diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp new file mode 100644 index 00000000..501cc664 --- /dev/null +++ b/panoramapublic/src/org/labkey/panoramapublic/view/createMessageForm.jsp @@ -0,0 +1,99 @@ +<% + /* + * 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.panoramapublic.PanoramaPublicController" %> +<%@ page import="org.labkey.api.view.JspView" %> +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.panoramapublic.PanoramaPublicNotification" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> + + +<% + JspView view = (JspView) HttpView.currentView(); + var form = view.getModelBean(); +%> + + + +
+
+ Enter a message that will be posted to the support message threads of the selected experiments submitted to Panorama Public. + Experiments can be selected in the "Panorama Public Experiments" grid below. +
+ + + + + + + + + + + + + + + + + +
Test Mode: + +
Message Title (prefix): + +
+ e.g. "Change Reviewer Password -". The title of the posted message will be: +
+ Change Reviewer Password - https://panoramaweb.org/expt-short-url.url +
+ where "expt-short-url.url" is the short URL assigned to the experiment. +
Message: + +
+ Use Markdown syntax - https://markdown-it.github.io +
+ The following link placeholders can be used: +
    +
  • <%=h(PanoramaPublicNotification.PLACEHOLDER_SHORT_URL)%> will be replaced with the Short URL for the data.
  • +
  • <%=h(PanoramaPublicNotification.PLACEHOLDER_MESSAGE_THREAD_URL)%> will be replaced with a link to the message thread.
  • +
  • <%=h(PanoramaPublicNotification.PLACEHOLDER_RESPOND_TO_MESSAGE_URL)%> will be replaced with a link to respond to the message thread.
  • +
  • <%=h(PanoramaPublicNotification.PLACEHOLDER_MAKE_DATA_PUBLIC_URL)%> will be replaced with a link to make the data public.
  • +
+
<%=button("Post Message").onClick("submitForm();")%>
+
+
+
\ No newline at end of file diff --git a/panoramapublic/src/org/labkey/panoramapublic/view/publish/publicationDetails.jsp b/panoramapublic/src/org/labkey/panoramapublic/view/publish/publicationDetails.jsp index 6cb28d03..282f4866 100644 --- a/panoramapublic/src/org/labkey/panoramapublic/view/publish/publicationDetails.jsp +++ b/panoramapublic/src/org/labkey/panoramapublic/view/publish/publicationDetails.jsp @@ -22,6 +22,13 @@ var form = bean.getForm(); %> + +
+ + + +
Title: <%=h(bean.getExperimentTitle())%>
Permanent Link: <%=link(bean.getAccessUrl(), bean.getAccessUrl()).clearClasses().build()%>
+