Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
1226215
WIP: Get galley annotation number through Hypothesis API
JhonathanLepidus Jul 20, 2022
4270fca
Finishes HypothesisClient class
JhonathanLepidus Jul 21, 2022
f91a402
Prepares the including of custom template in preprint landing page
JhonathanLepidus Jul 22, 2022
62a38d8
Creates the template to add the annotation viewer to landing page
JhonathanLepidus Jul 22, 2022
ea10bc6
Removes left margin of annotation viewer
JhonathanLepidus Jul 26, 2022
c755f8f
Switch annotation viewer addin strategy
JhonathanLepidus Jul 26, 2022
9912be6
Removes commented code
JhonathanLepidus Jul 26, 2022
80d50b4
Brings Hypothesis API call to the template
JhonathanLepidus Jul 27, 2022
f6e5fbd
Adds Hypothesis config template
JhonathanLepidus Aug 2, 2022
9a1eab7
Uses load event for configuring Hypothesis client
JhonathanLepidus Aug 3, 2022
2b64509
Hypothesis sidebar opens when clicking in annotation number
JhonathanLepidus Aug 4, 2022
a502236
Redesigns annotation viewer to use the same link of the galley
JhonathanLepidus Aug 5, 2022
4334c65
Creates HypothesisHandler class
JhonathanLepidus Aug 19, 2022
f15100b
Adds page to list submissions with annotations
JhonathanLepidus Aug 19, 2022
4b05163
Fixes bug occurred when galleys did not have file
JhonathanLepidus Aug 22, 2022
a118d42
Requests Hypothesis API for annotations by submissions chunks
JhonathanLepidus Aug 22, 2022
f16d57d
Fixes problem with submissions groups annotations requests
JhonathanLepidus Aug 30, 2022
dbf5996
Get submission object for submissions with annotations
JhonathanLepidus Aug 30, 2022
6f11bb0
Stores submissions with annotations in cache
JhonathanLepidus Aug 31, 2022
8486eb3
Set annotations cache to updated through scheduled task
JhonathanLepidus Sep 12, 2022
e98980b
Fixes bug when adding annotation number viewer
JhonathanLepidus Sep 12, 2022
09e1351
Creates page for listing of submissions with annotations
JhonathanLepidus Sep 12, 2022
2e81266
Sets locales for annotations page
JhonathanLepidus Sep 12, 2022
be5214a
Fix bug occurred for submissions without publications
JhonathanLepidus Sep 13, 2022
45a2971
Handler returns submissions' annotations
JhonathanLepidus Sep 14, 2022
e50a87f
Sets annotations retrieving from cache
JhonathanLepidus Sep 16, 2022
d8214ac
Set up the display of annotations
richardauzier-afk Sep 19, 2022
55ccaea
Adds styling for annotations
JhonathanLepidus Sep 19, 2022
e73ffd9
Adds Annotation class
JhonathanLepidus Sep 22, 2022
8229782
Retrieves annotation data from API response
JhonathanLepidus Sep 22, 2022
4f17353
Exhibits new annotation info in annotations page
JhonathanLepidus Sep 22, 2022
f7bc301
WIP: Read more for annotation targets
JhonathanLepidus Sep 26, 2022
521a488
Adds read more button for annotation targets
JhonathanLepidus Sep 26, 2022
84fc02e
Adds read more button for annotation content
JhonathanLepidus Sep 26, 2022
95c2247
Adds submission metadata to annotations page
JhonathanLepidus Sep 26, 2022
3486656
Remove search box
lucasDevLepidus Oct 4, 2022
8e32bfa
Adds order by date of last annotation
JhonathanLepidus Feb 3, 2023
a0a8554
Adds ordering by date published
JhonathanLepidus Feb 3, 2023
bd4a865
Adds select to order of submissions in annotations page
JhonathanLepidus Feb 6, 2023
f1f146a
OrderBy is selected according to page's URL
JhonathanLepidus Feb 6, 2023
7e7a1a3
Changes annotations page ordering when one is selected
JhonathanLepidus Feb 7, 2023
27783eb
Keeps selected order through annotations pagination
JhonathanLepidus Feb 7, 2023
58431e3
Refactors code of annotations ordering functions
JhonathanLepidus Feb 14, 2023
ba8d71e
Changes default ordering to order by last annotation
JhonathanLepidus Mar 9, 2023
b7f9328
Escapes annotations content in annotations page
JhonathanLepidus Feb 7, 2024
3c8c56e
Makes plugin fully compatible with OJS and OPS
JhonathanLepidus Mar 22, 2024
b98e0b6
Minor fixes in annotations page
JhonathanLepidus Mar 22, 2024
dcfd8ae
Fixes error in task for updating annotations cache
JhonathanLepidus Nov 7, 2024
aaf7b86
Indents code using tabs instead of spaces
JhonathanLepidus Nov 7, 2024
aa316ff
Fixes minor leftover error in indentation
JhonathanLepidus Nov 7, 2024
e87fbc7
Fixes error when adding javascript to frontend
JhonathanLepidus Nov 7, 2024
5384163
Fixes bugs with retrieving of submissions annotations
JhonathanLepidus Dec 18, 2024
9d031a1
Adds NMI type for annotations page
JhonathanLepidus Aug 4, 2025
ca67563
Adds navigation menu item when plugin is enabled
JhonathanLepidus Aug 4, 2025
44a5505
Renames locale keys for navigation menu item
JhonathanLepidus Aug 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 152 additions & 5 deletions HypothesisPlugin.inc.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,26 @@

import('lib.pkp.classes.plugins.GenericPlugin');

define('NMI_TYPE_ANNOTATIONS', 'NMI_TYPE_ANNOTATIONS');

class HypothesisPlugin extends GenericPlugin {
/**
* @copydoc Plugin::register()
*/
function register($category, $path, $mainContextId = null) {
if (parent::register($category, $path, $mainContextId)) {
HookRegistry::register('ArticleHandler::download',array(&$this, 'callback'));
HookRegistry::register('ArticleHandler::download', array(&$this, 'callback'));
HookRegistry::register('TemplateManager::display', array(&$this, 'callbackTemplateDisplay'));
HookRegistry::register('TemplateManager::display', [$this, 'addAnnotationNumberViewers']);
HookRegistry::register('LoadHandler', array($this, 'addAnnotationsHandler'));
HookRegistry::register('LoadComponentHandler', array($this, 'setupHypothesisHandler'));
HookRegistry::register('AcronPlugin::parseCronTab', [$this, 'addTasksToCrontab']);

HookRegistry::register('NavigationMenus::itemTypes', array($this, 'addNavigationMenuItemType'));
HookRegistry::register('NavigationMenus::displaySettings', array($this, 'setNavigationMenuItemUrl'));

$this->addHandlerURLToJavaScript();

return true;
}
return false;
Expand Down Expand Up @@ -55,12 +67,13 @@ function callbackTemplateDisplay($hookName, $args) {
$templateMgr = $args[0];
$template = $args[1];
$plugin = 'plugins-generic-pdfJsViewer';
$submissiontpl = 'submissionGalley.tpl';
$issuetpl = 'issueGalley.tpl';
$submissionGalleyTpl = 'submissionGalley.tpl';
$issueGalleyTpl = 'issueGalley.tpl';

// template path contains the plugin path, and ends with the tpl file
if ( (strpos($template, $plugin) !== false) && ( (strpos($template, ':'.$submissiontpl, -1 - strlen($submissiontpl)) !== false) || (strpos($template, ':'.$issuetpl, -1 - strlen($issuetpl)) !== false))) {
if ( (strpos($template, $plugin) !== false) && ( (strpos($template, ':'.$submissionGalleyTpl, -1 - strlen($submissionGalleyTpl)) !== false) || (strpos($template, ':'.$issueGalleyTpl, -1 - strlen($issueGalleyTpl)) !== false))) {
$templateMgr->registerFilter("output", array($this, 'changePdfjsPath'));
$templateMgr->registerFilter("output", array($this, 'addHypothesisConfig'));
}
return false;
}
Expand All @@ -76,6 +89,114 @@ function changePdfjsPath($output, $templateMgr) {
return $newOutput;
}

/**
* Adds Hypothesis tab configuration so sidebar opens automatically when PDF has annotations
* @param $output string
* @param $templateMgr TemplateManager
* @return $string
*/
public function addHypothesisConfig($output, $templateMgr) {
if (preg_match('/<div[^>]+id="pdfCanvasContainer/', $output, $matches, PREG_OFFSET_CAPTURE)) {
$posMatch = $matches[0][1];
$config = $templateMgr->fetch($this->getTemplateResource('hypothesisConfig.tpl'));

$output = substr_replace($output, $config, $posMatch, 0);
$templateMgr->unregisterFilter('output', array($this, 'addHypothesisConfig'));
}
return $output;
}

public function addAnnotationNumberViewers($hookName, $args) {
$templateMgr = $args[0];
$template = $args[1];
$pagesToInsert = [
'frontend/pages/indexServer.tpl',
'frontend/pages/preprint.tpl',
'frontend/pages/preprints.tpl',
'frontend/pages/sections.tpl',
'frontend/pages/indexJournal.tpl',
'frontend/pages/article.tpl',
'frontend/pages/issue.tpl'
];

if (in_array($template, $pagesToInsert)) {
$request = Application::get()->getRequest();

$jsUrl = $request->getBaseUrl() . '/' . $this->getPluginPath() . '/js/addAnnotationViewers.js';
$styleUrl = $request->getBaseUrl() . '/' . $this->getPluginPath() . '/styles/annotationViewer.css';

$templateMgr->addJavascript('AddAnnotationViewers', $jsUrl, ['contexts' => 'frontend']);
$templateMgr->addStyleSheet('AnnotationViewerStyleSheet', $styleUrl, ['contexts' => 'frontend']);
}

return false;
}

public function addAnnotationsHandler($hookName, $args) {
$page = $args[0];
if ($page == 'annotations') {
$this->import('pages.annotations.AnnotationsHandler');
define('HANDLER_CLASS', 'AnnotationsHandler');
return true;
}
return false;
}

public function setupHypothesisHandler($hookName, $args) {
$component = &$args[0];
if ($component == 'plugins.generic.hypothesis.controllers.HypothesisHandler') {
return true;
}
return false;
}

public function addTasksToCrontab($hookName, $args) {
$taskFilesPath = &$args[0];
$taskFilesPath[] = $this->getPluginPath() . DIRECTORY_SEPARATOR . 'scheduledTasks.xml';
return false;
}

public function addHandlerURLToJavaScript()
{
$request = Application::get()->getRequest();
$context = $request->getContext();

if ($context) {
$templateMgr = TemplateManager::getManager($request);
$handlerUrl = $request->getDispatcher()->url($request, ROUTE_COMPONENT, null, 'plugins.generic.hypothesis.controllers.HypothesisHandler');
$data = ['hypothesisHandlerUrl' => $handlerUrl];

$templateMgr->addJavaScript('HypothesisHandler', 'app = ' . json_encode($data) . ';', ['contexts' => 'frontend', 'inline' => true]);
}
}

public function addNavigationMenuItemType($hookName, $args) {
$itemTypes = &$args[0];
$itemTypes[NMI_TYPE_ANNOTATIONS] = [
'title' => __('plugins.generic.hypothesis.annotationsMenuItem.title'),
'description' => __('plugins.generic.hypothesis.annotationsMenuItem.description'),
];
return false;
}

public function setNavigationMenuItemUrl($hookName, $args) {
$menuItem = &$args[0];

if ($menuItem->getType() === NMI_TYPE_ANNOTATIONS) {
$request = Application::get()->getRequest();
$dispatcher = $request->getDispatcher();

$menuItem->setUrl($dispatcher->url(
$request,
ROUTE_PAGE,
null,
'annotations',
null,
null
));
}
}

/**
* Get the display name of this plugin
* @return string
Expand All @@ -91,5 +212,31 @@ function getDisplayName() {
function getDescription() {
return __('plugins.generic.hypothesis.description');
}

function getStyleSheet() {
return $this->getPluginPath() . '/styles/annotationViewer.css';
}

public function setEnabled($enabled)
{
parent::setEnabled($enabled);

$contextId = $this->getCurrentContextId();
if ($enabled && $contextId != CONTEXT_SITE) {
$navigationMenuItemDao = DAORegistry::getDAO('NavigationMenuItemDAO');
$menuItems = $navigationMenuItemDao->getByType(NMI_TYPE_ANNOTATIONS, $contextId)->toArray();

if(empty($menuItems)) {
$locale = AppLocale::getLocale();

$menuItem = $navigationMenuItemDao->newDataObject();
$menuItem->setTitle(__('plugins.generic.hypothesis.annotationsMenuItem.title'), $locale);
$menuItem->setContextId($contextId);
$menuItem->setType(NMI_TYPE_ANNOTATIONS);

$navigationMenuItemDao->insertObject($menuItem);
}
}
}
}

20 changes: 20 additions & 0 deletions classes/Annotation.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

class Annotation {
public $user;
public $dateCreated;
public $target;
public $content;

public function __construct(string $user, string $dateCreated, string $target, string $content) {
$this->user = $user;
$this->dateCreated = $dateCreated;
$this->target = $target;
$this->content = $content;
}

public static function __set_state($dump) {
$obj = new Annotation($dump['user'], $dump['dateCreated'], $dump['target'], $dump['content']);
return $obj;
}
}
36 changes: 36 additions & 0 deletions classes/HypothesisDAO.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

import('lib.pkp.classes.db.DAO');

use Illuminate\Database\Capsule\Manager as Capsule;
use Illuminate\Support\Collection;

class HypothesisDAO extends DAO {
public function getDatePublished($submissionId): string {
$result = Capsule::table('submissions')
->where('submission_id', $submissionId)
->select('current_publication_id')
->first();
$currentPublicationId = get_object_vars($result)['current_publication_id'];

$result = Capsule::table('publications')
->where('publication_id', $currentPublicationId)
->select('date_published')
->first();

return get_object_vars($result)['date_published'];
}

public function getSubmissionIdByBestId($submissionBestId): int {
$result = Capsule::table('publications')
->where('url_path', $submissionBestId)
->select('submission_id')
->first();

if (!is_null($result)) {
return (int) get_object_vars($result)['submission_id'];
}

return (int) $submissionBestId;
}
}
140 changes: 140 additions & 0 deletions classes/HypothesisHelper.inc.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

import('classes.submission.Submission');
import('plugins.generic.hypothesis.classes.SubmissionAnnotations');
import('plugins.generic.hypothesis.classes.HypothesisDAO');

class HypothesisHelper {
public function getSubmissionsAnnotations($contextId) {
$submissions = Services::get('submission')->getMany([
'contextId' => $contextId,
'status' => STATUS_PUBLISHED
]);

$groupsRequests = $this->getSubmissionsGroupsRequests($submissions, $contextId);
$submissionsAnnotations = [];
foreach ($groupsRequests as $groupRequest) {
$groupResponse = $this->getRequestAnnotations($groupRequest);
if (!is_null($groupResponse) && $groupResponse['total'] > 0) {
$submissionsAnnotations = array_merge(
$submissionsAnnotations,
$this->groupSubmissionsAnnotations($groupResponse)
);
}
}

return $submissionsAnnotations;
}

private function getSubmissionsGroupsRequests($submissions, $contextId) {
$requests = [];
$requestPrefix = $currentRequest = "https://hypothes.is/api/search?limit=200&group=__world__";
$maxRequestLength = 4094;

foreach ($submissions as $submission) {
$submissionRequestParams = $this->getSubmissionRequestParams($submission, $contextId);

if(!is_null($submissionRequestParams)) {
if(strlen($currentRequest.$submissionRequestParams) < $maxRequestLength) {
$currentRequest .= $submissionRequestParams;
}
else {
$requests[] = $currentRequest;
$currentRequest = $requestPrefix . $submissionRequestParams;
}
}
}

if ($currentRequest != $requestPrefix) {
$requests[] = $currentRequest;
}

return $requests;
}

private function getSubmissionRequestParams($submission, $contextId) {
$submissionRequestParams = "";
$publication = $submission->getCurrentPublication();

if(is_null($publication))
return null;

$galleys = $publication->getData('galleys');
foreach ($galleys as $galley) {
if ($galley->getFileType() == 'application/pdf') {
$galleyDownloadURL = $this->getGalleyDownloadURL($contextId, $submission, $galley);

if(!is_null($galleyDownloadURL)) {
$submissionRequestParams .= "&uri={$galleyDownloadURL}";
}
}
}

return $submissionRequestParams;
}

private function getRequestAnnotations($requestURL) {
$ch = curl_init($requestURL);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
if(!$output || substr($output, 1, 8) != '"total":') return null;

return json_decode($output, true);
}

private function groupSubmissionsAnnotations($groupResponse) {
$submissionsAnnotations = [];
$hypothesisDao = new HypothesisDAO();

foreach ($groupResponse['rows'] as $annotationResponse) {
$urlBySlash = explode("/", $annotationResponse['links']['incontext']);
$submissionBestId = $urlBySlash[count($urlBySlash) - 3];
$submissionId = $hypothesisDao->getSubmissionIdByBestId($submissionBestId);

if(!array_key_exists($submissionId, $submissionsAnnotations)) {
$submissionsAnnotations[$submissionId] = new SubmissionAnnotations($submissionId);
}

$annotation = $this->getAnnotation($annotationResponse);
$submissionsAnnotations[$submissionId]->addAnnotation($annotation);
}

return $submissionsAnnotations;
}

private function getAnnotation($annotationResponse): Annotation {
$user = substr($annotationResponse['user'], 5, strlen($annotationResponse['user']) - 17);
$dateCreated = $annotationResponse['created'];
$content = $annotationResponse['text'];

$target = "";
if(isset($annotationResponse['target'][0]['selector'])) {
foreach ($annotationResponse['target'][0]['selector'] as $selector) {
if($selector['type'] == 'TextQuoteSelector') {
$target = $selector['exact'];
break;
}
}
}

return new Annotation($user, $dateCreated, $target, $content);
}

public function getGalleyDownloadURL($contextId, $submission, $galley) {
$request = Application::get()->getRequest();
$indexUrl = $request->getIndexUrl();
$context = Services::get('context')->get($contextId);
$contextPath = $context->getPath();
$submissionType = (Application::getName() == 'ojs2' ? 'article' : 'preprint');

$submissionFile = $galley->getFile();
if(is_null($submissionFile))
return null;

$submissionBestId = $submission->getBestId();
$galleyBestId = $galley->getBestGalleyId();
$fileId = $submissionFile->getId();

return $indexUrl . "/$contextPath/$submissionType/download/$submissionBestId/$galleyBestId/$fileId";
}
}
Loading