Socially Networked Open Peer Review

From PKP Wiki
Revision as of 06:37, 21 December 2011 by 180.253.126.226 (Talk)

Jump to: navigation, search

Here are some notes on how we (the World Economics Association) implemented anonymous, socially networked, open peer review for our PKP hosted journals. We did this by replacing PKP's commenting system with the one provided by Disqus which integrates itself into all the main social networking systems.

Disqus also allows content voting and comment voting, so visitors can vote "Like" or "Dislike" on an article, and/or comments about an article. One can then use this voting and commenting information to decide if an article ought to be published. Needless to say, one can also share articles and comments with various social networking platforms, thus drawing in more readers and reviewers.

Before you begin

You're going to need an account on disqus and almost certainly another paid one for akismet to automate spam filtering if you're going to allow public commenting. You're going to need a thing called an "API key" from disqus, and it HAS to be of the "public" rather than secret variety which requires a certain amount of configuring. If you run into troubles, search google as lots of other people struggle with the paucity of disqus documentation.

You'll also need a SSH account on the PKP's servers (ask PKP) and someone competent with Web technologies. If you don't have one of these to hand, you can try employing me if I'm available or finding someone on oDesk. I'll be blunt by saying that someone from oDesk will be a lot cheaper, I simply can't compete with developing world prices for basic Web technologies expertise.

And finally, you'll need to configure PKP for open peer review according to this FAQ question: http://pkp.sfu.ca/wiki/index.php/PKP_Frequently_Asked_Questions#Does_OJS_support_open_peer_review.3F

Replacing commenting with Disqus

Firstly, we replace the per-article commenting. Find ~/ojs/templates/article/comments.tpl, copy as comments.tpl.old, and replace its contents as follows:

{* Complete replacement
Niall Douglas
2011-09-11
*}


<!-- AddThis Button BEGIN -->
<div class="addthis_toolbox addthis_default_style ">
<a class="addthis_button_facebook_like" fb:like:layout="button_count"></a>
<a class="addthis_button_tweet"></a>
<a class="addthis_button_google_plusone" g:plusone:size="medium"></a>
<a class="addthis_counter addthis_pill_style"></a>
</div>
<script type="text/javascript" src="http://s7.addthis.com/js/250/addthis_widget.js#pubid=ra-xxxxxxxxxxxxxxxx"></script>
<!-- AddThis Button END -->

{if $comments}
<div class="separator"></div>
<div id="commentsOnArticle">
<h4>Did you like or dislike this article? Please vote here - your votes influence article selection!</h4>
<div id="disqus_thread"></div>
<script type="text/javascript"><!--
    var disqus_shortname = 'wej';
    var disqus_identifier = '{$currentJournal->getLocalizedTitle()|strip_tags|escape}|{foreach name="authors" from=$article->getAuthors() item=author}{$author->getLastName()|escape}, {$author->getFirstName()|escape}{if $author->getMiddleName() != ""} {$author->getMiddleName()|escape}{/if}{if !$smarty.foreach.authors.last}; {/if}{/foreach}|{$article->getLocalizedTitle()|strip_tags|escape}|{$article->getDatePublished()|date_format:"%d/%m/%Y"}';

{literal}
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
{/literal}
// -->
</script>
<noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
<a href="http://disqus.com" class="dsq-brlink">blog comments powered by <span class="logo-disqus">Disqus</span></a>
</div>
{/if}{* $comments *}

The first bit of XHTML adds an "AddThis" button through which the article can be shared via social networking. You'll need your own snippet here with its own unique ID - get this from http://addthis.com.

After replacing the above, test to make sure it works. The comments are stored on Disqus under the identifier:

<Journal Name>|<Authors>|<Article Title>|<Publication Date>

This lets you reuse a comments stream for a revised article if you wish by dropping the publication date.

Displaying comment metadata in the review issue

Next thing is to display this new metadata in the issue table of contents. Find ~/ojs/templates/issue/issue.tpl, copy as issue.tpl.old and edit to look like this:

WARNING: The code below is still under development. I wouldn't use it until I remove this warning message. It has several known bugs at present!
{**
 * issue.tpl
 *
 * Copyright (c) 2003-2011 John Willinsky
 * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING.
 *
 * Issue
 *
 * $Id$
 *} 
{foreach name=sections from=$publishedArticles item=section key=sectionId}
{if $section.title}<h4 class="tocSectionTitle">{$section.title|escape}</h4>{/if}

{foreach from=$section.articles item=article}
	{assign var=articlePath value=$article->getBestArticleId($currentJournal)}
	
<table class="tocArticle" width="100%">
<tr valign="top">
	{if $article->getLocalizedFileName() && $article->getLocalizedShowCoverPage() && !$article->getHideCoverPageToc($locale)}
	<td rowspan="2">
		<div class="tocArticleCoverImage">
		<a href="{url page="article" op="view" path=$articlePath}" class="file">
		<img src="{$coverPagePath|escape}{$article->getFileName($locale)|escape}"{if $article->getCoverPageAltText($locale) != ''} alt="{$article->getCoverPageAltText($locale)|escape}"{else} alt="{translate key="article.coverPage.altText"}"{/if}/></a></div>
	</td>
	{/if}
	{call_hook name="Templates::Issue::Issue::ArticleCoverImage"}

	{if $article->getLocalizedAbstract() == ""}
		{assign var=hasAbstract value=0}
	{else}
		{assign var=hasAbstract value=1}
	{/if}

	{assign var=articleId value=$article->getId()}
	{if (!$subscriptionRequired || $article->getAccessStatus() == $smarty.const.ARTICLE_ACCESS_OPEN || $subscribedUser || $subscribedDomain || ($subscriptionExpiryPartial && $articleExpiryPartial.$articleId))}
		{assign var=hasAccess value=1}
	{else}
		{assign var=hasAccess value=0}
	{/if}

	<td class="tocTitle">{if !$hasAccess || $hasAbstract}<a href="{url page="article" op="view" path=$articlePath}">{$article->getLocalizedTitle()|strip_unsafe_html}</a>{else}{$article->getLocalizedTitle()|strip_unsafe_html}{/if}</td>
	<td class="tocGalleys">

{* 2011-09-11 Niall Douglas like/dislike insert *}
<span class="disquscounts" data-disqus-identifier="{$currentJournal->getLocalizedTitle()|strip_tags|escape}|{foreach name="authors" from=$article->getAuthors() item=author}{$author->getLastName()|escape}, {$author->getFirstName()|escape}{if $author->getMiddleName() != ""} {$author->getMiddleName()|escape}{/if}{if !$smarty.foreach.authors.last}; {/if}{/foreach}|{$article->getLocalizedTitle()|strip_tags|escape}|{$article->getDatePublished()|date_format:"%d/%m/%Y"}"></span>

		{if $hasAccess || ($subscriptionRequired && $showGalleyLinks)}
			{foreach from=$article->getGalleys() item=galley name=galleyList}
				<a href="{url page="article" op="view" path=$articlePath|to_array:$galley->getBestGalleyId($currentJournal)}" class="file">{$galley->getGalleyLabel()|escape}</a>
				{if $subscriptionRequired && $showGalleyLinks && $restrictOnlyPdf}
					{if $article->getAccessStatus() == $smarty.const.ARTICLE_ACCESS_OPEN || !$galley->isPdfGalley()}	
						<img class="accessLogo" src="{$baseUrl}/lib/pkp/templates/images/icons/fulltext_open_medium.gif" alt="{translate key="article.accessLogoOpen.altText"}" />
					{else}
						<img class="accessLogo" src="{$baseUrl}/lib/pkp/templates/images/icons/fulltext_restricted_medium.gif" alt="{translate key="article.accessLogoRestricted.altText"}" />
					{/if}
				{/if}
			{/foreach}
			{if $subscriptionRequired && $showGalleyLinks && !$restrictOnlyPdf}
				{if $article->getAccessStatus() == $smarty.const.ARTICLE_ACCESS_OPEN}
					<img class="accessLogo" src="{$baseUrl}/lib/pkp/templates/images/icons/fulltext_open_medium.gif" alt="{translate key="article.accessLogoOpen.altText"}" />
				{else}
					<img class="accessLogo" src="{$baseUrl}/lib/pkp/templates/images/icons/fulltext_restricted_medium.gif" alt="{translate key="article.accessLogoRestricted.altText"}" />
				{/if}
			{/if}				
		{/if}
	</td>
</tr>
<tr>
	<td class="tocAuthors">
		{if (!$section.hideAuthor && $article->getHideAuthor() == 0) || $article->getHideAuthor() == 2}
			{foreach from=$article->getAuthors() item=author name=authorList}
				{$author->getFullName()|escape}{if !$smarty.foreach.authorList.last},{/if}
			{/foreach}
		{else}
			 
		{/if}
	</td>
	<td class="tocPages">{$article->getPages()|escape}</td>
</tr>
</table>
{/foreach}

{* 2011-09-11 Niall Douglas like/dislike insert *}
<script type="text/javascript"><!--
{literal}
var items=$("span.disquscounts");
var idents="";
items.each(function() { idents+="&thread="+escape("ident:"+$(this).attr("data-disqus-identifier")); });

$.getJSON("http://disqus.com/api/3.0/threads/list.jsonp?callback=?"+idents,
         {
            api_key: "<insert your public API key here>",
            forum: "<insert your forum short name here>"
         },
         function(data) {
            items.each(function() {
               var ident=$(this).attr("data-disqus-identifier");
               for(var i=0; i<data.response.length; i++) {
                  if(data.response[i].identifiers[0]==ident) {
                     var likes=data.response[i].likes, dislikes=data.response[i].dislikes, comments=data.response[i].posts;
                     var s="";
                     s+='<span class="disquscountlikes">'+likes+(1==likes ? " Like" : " Likes")+"</span>, ";
                     s+='<span class="disquscountdislikes">'+dislikes+(1==dislikes ? " Dislike" : " Dislikes")+"</span>, ";
                     s+='<span class="disquscountcomments">'+comments+(1==comments ? " Comment" : " Comments")+"</span> ";
                     $(this).html(s);
                  }
               }
            });
         });
{/literal}
// -->
</script>

{if !$smarty.foreach.sections.last}
<div class="separator"></div>
{/if}
{/foreach}

You'll see that this relies on jQuery to run the query client-side. This isn't ideal - really we ought to pull the metadata from disqus using PHP and cache it somewhere, but as a quick and dirty solution it works so long as there aren't more than 1000 views of the review issue's table of contents per hour. Link title * Gadgets