diff --git a/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java b/panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java index 3af3cd60..d8e1a6f1 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; @@ -133,6 +135,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; @@ -155,8 +159,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; @@ -239,7 +243,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; @@ -299,7 +302,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; @@ -325,12 +328,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 @@ -6668,6 +6676,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; @@ -6675,6 +6685,7 @@ public PublicationDetailsBean(PublicationDetailsForm form, ExperimentAnnotations _isPeerReviewed = copiedExperiment.isPeerReviewed(); _accessUrl = copiedExperiment.getShortUrl().renderShortURL(); _license = copiedExperiment.getDataLicense(); + _experimentTitle = copiedExperiment.getTitle(); } public PublicationDetailsForm getForm() @@ -6701,6 +6712,11 @@ public DataLicense getLicense() { return _license; } + + public String getExperimentTitle() + { + return _experimentTitle; + } } public static class PublishSuccessViewBean @@ -9206,100 +9222,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 a67feaa4..4c93fbea 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; @@ -72,6 +73,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; @@ -246,6 +248,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) @@ -405,6 +413,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 3705c6d6..5168375a 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()%>
+