Skip to content

Commit 5b1c4df

Browse files
authored
Search NCBI for papers associated with private datasets (#606)
- Added a setting in the admin console (Private Data Reminder Settings) to enable publication search - Added NcbiPublicationSearchService that searches for publications in -- PubMed Central (full-text available): search terms ProteomeXchange Id, Panorama Public short URL, Panorama Public DOI. Author and title verified. -- PubMed - fallback if no matches found in PMC; search author and title only since PubMed does not have full-text articles -- Preprints (e.g. biorxiv, medrxiv etc.) are filtered out -- Results are prioritized by number of matched search terms (e.g. PX ID, Panorama link) as well as paper publication date proximity to data submission date - Publication search happens automatically in the PrivateDataReminderJob when enabled in the admin console - Publications for a single dataset can also be searched through the Search Publications menu item in the _TargetedMS Experiment_ webpart menu - If a publication match is found during the PrivateDataReminderJob, instead of the usual reminder message, a "publication found" message is posted to the submitter - Submitter can dismiss suggested publication by clicking the Dismiss Publication Suggestion link in the message. - DatasetStatus caches publication result to avoid repeated calls to NCBI's EUtils endpoints - Moved NCBI citation lookup methods to NcbiPublicationSearchServiceImpl - **Tests** -- Unit tests added in NcbiPublicationSearchServiceImpl -- Added Selenium test PublicationSearchTest -- Test uses a mock NCBI service on TeamCity (mocking only the outbound HTTP requests) so all real search and filtering logic is exercised without live API calls.
1 parent 389a437 commit 5b1c4df

22 files changed

Lines changed: 3944 additions & 178 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
-- Add publication tracking columns to DatasetStatus
3+
ALTER TABLE panoramapublic.DatasetStatus ADD COLUMN PotentialPublicationId VARCHAR(255);
4+
ALTER TABLE panoramapublic.DatasetStatus ADD COLUMN PublicationType VARCHAR(50);
5+
ALTER TABLE panoramapublic.DatasetStatus ADD COLUMN PublicationMatchInfo TEXT;
6+
ALTER TABLE panoramapublic.DatasetStatus ADD COLUMN Citation TEXT;
7+
ALTER TABLE panoramapublic.DatasetStatus ADD COLUMN UserDismissedPublication TIMESTAMP;

panoramapublic/resources/schemas/panoramapublic.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,21 @@
864864
<column columnName="LastReminderDate" />
865865
<column columnName="ExtensionRequestedDate" />
866866
<column columnName="DeletionRequestedDate"/>
867+
<column columnName="PotentialPublicationId">
868+
<description>Publication ID (PubMed ID, PMC ID) suggestion based on NCBI search</description>
869+
</column>
870+
<column columnName="PublicationType">
871+
<description>Type of publication ID: PMID, PMC</description>
872+
</column>
873+
<column columnName="PublicationMatchInfo">
874+
<description>Information about which fields (e.g. PXD, Panorama link, title etc.) matched</description>
875+
</column>
876+
<column columnName="Citation">
877+
<description>NLM citation string for the potential publication match</description>
878+
</column>
879+
<column columnName="UserDismissedPublication">
880+
<description>Date when the user dismissed the publication suggestion for this dataset</description>
881+
</column>
867882
</columns>
868883
</table>
869884
</tables>

panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicController.java

Lines changed: 617 additions & 20 deletions
Large diffs are not rendered by default.

panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
import org.labkey.panoramapublic.bluesky.PanoramaPublicLogoResourceType;
4848
import org.labkey.panoramapublic.catalog.CatalogImageAttachmentType;
4949
import org.labkey.panoramapublic.message.PrivateDataReminderSettings;
50+
import org.labkey.panoramapublic.ncbi.NcbiPublicationSearchServiceImpl;
5051
import org.labkey.panoramapublic.model.Journal;
5152
import org.labkey.panoramapublic.model.speclib.SpecLibKey;
5253
import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineProvider;
@@ -92,7 +93,7 @@ public String getName()
9293
@Override
9394
public @Nullable Double getSchemaVersion()
9495
{
95-
return 25.003;
96+
return 25.004;
9697
}
9798

9899
@Override
@@ -382,6 +383,7 @@ public Set<String> getSchemaNames()
382383
set.add(CatalogEntryManager.TestCase.class);
383384
set.add(BlueskyApiClient.TestCase.class);
384385
set.add(PrivateDataReminderSettings.TestCase.class);
386+
set.add(NcbiPublicationSearchServiceImpl.TestCase.class);
385387

386388
return set;
387389
}

panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.labkey.panoramapublic.model.JournalExperiment;
2929
import org.labkey.panoramapublic.model.JournalSubmission;
3030
import org.labkey.panoramapublic.model.Submission;
31+
import org.labkey.panoramapublic.ncbi.PublicationMatch;
3132
import org.labkey.panoramapublic.proteomexchange.ProteomeXchangeService;
3233
import org.labkey.panoramapublic.query.ExperimentAnnotationsManager;
3334
import org.labkey.panoramapublic.query.JournalManager;
@@ -358,22 +359,63 @@ public static void postDataDeletionRequestMessage(@NotNull Journal journal, @Not
358359
postNotificationFullTitle(journal, je, messageBody.toString(), journalAdmin, messageTitle, AnnouncementService.StatusOption.Active, null);
359360
}
360361

362+
public static void postPublicationDismissalMessage(@NotNull Journal journal, @NotNull JournalExperiment je, @NotNull ExperimentAnnotations expAnnotations, User submitter,
363+
@NotNull PublicationMatch publicationMatch)
364+
{
365+
User journalAdmin = JournalManager.getJournalAdminUser(journal);
366+
if (journalAdmin == null)
367+
{
368+
throw new NotFoundException(String.format("Could not find an admin user for %s.", journal.getName()));
369+
}
370+
371+
String messageTitle = "Publication Suggestion Dismissed" +" - " + expAnnotations.getShortUrl().renderShortURL();
372+
373+
StringBuilder messageBody = new StringBuilder();
374+
messageBody.append("Dear ").append(getUserName(submitter)).append(",").append(NL2);
375+
messageBody.append("Thank you for letting us know that the suggested paper is not associated with your data on Panorama Public.");
376+
377+
messageBody.append(NL2).append(bold("Dismissed Publication:")).append(" ").append(publicationMatch.getPublicationIdLabel());
378+
if (!StringUtils.isBlank(publicationMatch.getCitation()))
379+
{
380+
messageBody.append(NL).append(publicationMatch.getCitation());
381+
}
382+
383+
messageBody.append(NL2).append("We will no longer suggest this paper for your dataset. ")
384+
.append("If you would like to make your data public, you can do so at any time ")
385+
.append("by clicking the \"Make Public\" button in your data folder, or by clicking this link: ")
386+
.append(bold(link("Make Data Public", PanoramaPublicController.getMakePublicUrl(expAnnotations.getId(), expAnnotations.getContainer()).getURIString())))
387+
.append(".");
388+
messageBody.append(NL2).append("Best regards,");
389+
messageBody.append(NL).append(getUserName(journalAdmin));
390+
391+
postNotificationFullTitle(journal, je, messageBody.toString(), journalAdmin, messageTitle, AnnouncementService.StatusOption.Closed, null);
392+
}
393+
361394

362395
public static void postPrivateDataReminderMessage(@NotNull Journal journal, @NotNull JournalSubmission js, @NotNull ExperimentAnnotations expAnnotations,
363396
@NotNull User submitter, @NotNull User messagePoster, List<User> notifyUsers,
364-
@NotNull Announcement announcement, @NotNull Container announcementsContainer, @NotNull User journalAdmin)
397+
@NotNull Announcement announcement, @NotNull Container announcementsContainer, @NotNull User journalAdmin,
398+
@Nullable PublicationMatch articleMatch)
365399
{
366-
String message = getDataStatusReminderMessage(expAnnotations, submitter, js, announcement, announcementsContainer, journalAdmin);
367-
String title = "Action Required: Status Update for Your Private Data on Panorama Public";
400+
String message = getDataStatusReminderMessage(expAnnotations, submitter, js, announcement, announcementsContainer, journalAdmin, articleMatch);
401+
String title = articleMatch != null
402+
? "Action Required: Publication Found for Your Data on Panorama Public"
403+
: "Action Required: Status Update for Your Private Data on Panorama Public";
368404
postNotificationFullTitle(journal, js.getJournalExperiment(), message, messagePoster, title, AnnouncementService.StatusOption.Closed, notifyUsers);
369405
}
370406

371407
public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations exptAnnotations, @NotNull User submitter,
372408
@NotNull JournalSubmission js,@NotNull Announcement announcement,
373-
@NotNull Container announcementContainer, @NotNull User journalAdmin)
409+
@NotNull Container announcementContainer, @NotNull User journalAdmin,
410+
@Nullable PublicationMatch articleMatch)
374411
{
375412
String shortUrl = exptAnnotations.getShortUrl().renderShortURL();
376-
String makePublicLink = PanoramaPublicController.getMakePublicUrl(exptAnnotations.getId(), exptAnnotations.getContainer()).getURIString();
413+
ActionURL makePublicUrl = PanoramaPublicController.getMakePublicUrl(exptAnnotations.getId(), exptAnnotations.getContainer());
414+
if (articleMatch != null && articleMatch.isPubMed() && articleMatch.getPublicationId() != null)
415+
{
416+
makePublicUrl.replaceParameter("pubmedId", articleMatch.getPublicationId());
417+
}
418+
String makePublicLink = makePublicUrl.getURIString();
377419
String dateString = DateUtil.formatDateTime(js.getLatestSubmission().getCreated(), PrivateDataReminderSettings.DATE_FORMAT_PATTERN);
378420

379421
ActionURL viewMessageUrl = new ActionURL("announcements", "thread", announcementContainer)
@@ -386,32 +428,55 @@ public static String getDataStatusReminderMessage(@NotNull ExperimentAnnotations
386428
ActionURL requestExtensionUrl = new ActionURL(PanoramaPublicController.RequestExtensionAction.class, exptAnnotations.getContainer())
387429
.addParameter("shortUrlEntityId", shortUrlEntityId);
388430

389-
ActionURL requesDeletionUrl = new ActionURL(PanoramaPublicController.RequestDeletionAction.class, exptAnnotations.getContainer())
431+
ActionURL requestDeletionUrl = new ActionURL(PanoramaPublicController.RequestDeletionAction.class, exptAnnotations.getContainer())
390432
.addParameter("shortUrlEntityId",shortUrlEntityId);
391433

434+
ActionURL dismissPublicationUrl = new ActionURL(PanoramaPublicController.DismissPublicationSuggestionAction.class, exptAnnotations.getContainer())
435+
.addParameter("shortUrlEntityId", shortUrlEntityId);
392436

393437
ExperimentAnnotations sourceExperiment = ExperimentAnnotationsManager.get(exptAnnotations.getSourceExperimentId());
394438

395439
StringBuilder message = new StringBuilder();
396-
message.append("Dear ").append(getUserName(submitter)).append(",").append(NL2)
397-
.append("We are reaching out regarding your data on Panorama Public (").append(shortUrl).append("), which has been private since ")
398-
.append(dateString).append(".")
399-
.append(NL2).append(bold("Title:")).append(" ").append(escape(exptAnnotations.getTitle()))
400-
.append(NL2).append(bold("Is the paper associated with this work already published?"))
401-
.append(NL).append("- If yes: Please make your data public by clicking the \"Make Public\" button in your folder or by clicking this link: ")
402-
.append(bold(link("Make Data Public", makePublicLink)))
403-
.append(". This helps ensure that your valuable research is easily accessible to the community.")
404-
.append(NL).append("- If not: You have a couple of options:")
405-
.append(NL).append(" - ").append(bold("Request an Extension")).append(" - If your paper is still under review, or you need additional time, please let us know by clicking ")
406-
.append(bold(link("Request Extension", requestExtensionUrl.getURIString()))).append(".")
407-
.append(NL).append(" - ").append(bold("Delete from Panorama Public")).append(" - If you no longer wish to host your data on Panorama Public, please click ")
408-
.append(bold(link("Request Deletion", requesDeletionUrl.getURIString()))).append(". ")
409-
.append("We will remove your data from Panorama Public.");
410-
if (sourceExperiment != null)
440+
message.append("Dear ").append(getUserName(submitter)).append(",").append(NL2);
441+
442+
if (articleMatch != null)
443+
{
444+
// Message variant when a publication was found
445+
message.append("We found a paper that appears to be associated with your private data on Panorama Public (").append(shortUrl).append(").")
446+
.append(NL2).append(bold("Title:")).append(" ").append(escape(exptAnnotations.getTitle()))
447+
.append(NL2).append(bold("Publication Found:")).append(" ")
448+
.append(articleMatch.getCitation() != null
449+
? link(articleMatch.getCitation(), articleMatch.getPublicationUrl())
450+
: link(articleMatch.getPublicationIdLabel(), articleMatch.getPublicationUrl()))
451+
.append(NL2).append("If this is indeed your paper, congratulations! We encourage you to make your data public so the research community can access it alongside your paper. ")
452+
.append("You can do this by clicking the \"Make Public\" button in your data folder or by clicking this link: ")
453+
.append(bold(link("Make Data Public", makePublicLink))).append(".")
454+
.append(articleMatch.isPubMed() ? " Please enter " + articleMatch.getPublicationId() + " in the PubMed ID field." : "")
455+
.append(NL2).append("If this paper is not associated with your data please let us know by clicking ")
456+
.append(bold(link("Dismiss Publication Suggestion", dismissPublicationUrl.getURIString())));
457+
}
458+
else
411459
{
412-
message.append(" However, your source folder (")
413-
.append(getContainerLink(sourceExperiment.getContainer()))
414-
.append(") will remain intact, allowing you to resubmit your data in the future if you wish.");
460+
// Original message variant when no publication was found
461+
message.append("We are reaching out regarding your data on Panorama Public (").append(shortUrl).append("), which has been private since ")
462+
.append(dateString).append(".")
463+
.append(NL2).append(bold("Title:")).append(" ").append(escape(exptAnnotations.getTitle()))
464+
.append(NL2).append(bold("Is the paper associated with this work already published?"))
465+
.append(NL).append("- If yes: Please make your data public by clicking the \"Make Public\" button in your folder or by clicking this link: ")
466+
.append(bold(link("Make Data Public", makePublicLink)))
467+
.append(". This helps ensure that your valuable research is easily accessible to the community.")
468+
.append(NL).append("- If not: You have a couple of options:")
469+
.append(NL).append(" - ").append(bold("Request an Extension")).append(" - If your paper is still under review, or you need additional time, please let us know by clicking ")
470+
.append(bold(link("Request Extension", requestExtensionUrl.getURIString()))).append(".")
471+
.append(NL).append(" - ").append(bold("Delete from Panorama Public")).append(" - If you no longer wish to host your data on Panorama Public, please click ")
472+
.append(bold(link("Request Deletion", requestDeletionUrl.getURIString()))).append(". ")
473+
.append("We will remove your data from Panorama Public.");
474+
if (sourceExperiment != null)
475+
{
476+
message.append(" However, your source folder (")
477+
.append(getContainerLink(sourceExperiment.getContainer()))
478+
.append(") will remain intact, allowing you to resubmit your data in the future if you wish.");
479+
}
415480
}
416481

417482
message.append(NL2).append("If you have any questions or need further assistance, please do not hesitate to respond to this message by ")

0 commit comments

Comments
 (0)