From 2eceb3139725e5870c3cacf5b25fda14ba6dd550 Mon Sep 17 00:00:00 2001
From: Owen Leonard <oleonard@myacpl.org>
Date: Thu, 14 Aug 2014 14:52:34 -0400
Subject: [PATCH] [PASSED QA] Bug 11876 [Follow-up] Add a diff view to staged MARC Records

This follow up makes some corrections to the previously added files and
adds some functionality to the diff: A Javascript diff plugin highlights
the differences between the records in the two columns.

Corrections made: Converted tab indentation to spaces, corrected GPL
statement.

File organization: Moved showdiffmarc.pl and .tt to /tools/ to match the
location of the page with which it functions,
tools/manage-marc-import.pl. Corrected permissions on showdiffmarc.pl
accordingly.

Updates to the template: Added standard includes inclucing header menu
and breadcrumbs; converted custom layout to YUI Grid standard.

To test, follow the test plan previously defined:

- Stage a MARC record batch which contains at least one record match for
  something already in your catalog.
- Locate the staged MARC record batch in Tools -> Manage staged records
  and click to view the contents.
- Find the record which matched an existing record and click the "View"
  link in the Diff column.
- The compare screen should include the header menu and breadcrumbs. The
  differences between your staged file and the existing record should be
  higlighted.
- You should be able to return to the MARC batch you were previously
  viewing by following the link in the breadcrumbs or the link at the
  bottom of the page.
- Confirm that the "About" page includes information about the new
  JavaScript plugin on the Licenses tab.

Signed-off-by: Nick Clemens <nick@quecheelibrary.org>

Signed-off-by: Jonathan Druart <jonathan.druart@biblibre.com>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
---
 koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js       |  160 ++++++++++++++++++++
 koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js   |   10 ++
 koha-tmpl/intranet-tmpl/prog/en/modules/about.tt   |    3 +
 .../prog/en/modules/tools/showdiffmarc.tt          |   89 ++++++-----
 tools/batch_records_ajax.pl                        |    2 +-
 tools/showdiffmarc.pl                              |   10 +-
 6 files changed, 227 insertions(+), 47 deletions(-)
 create mode 100644 koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js
 create mode 100644 koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js

diff --git a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js
new file mode 100644
index 0000000..3e3cc47
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.js
@@ -0,0 +1,160 @@
+/*
+ * Javascript Diff Algorithm
+ *  By John Resig (http://ejohn.org/)
+ *  Modified by Chu Alan "sprite"
+ *
+ * Released under the MIT license.
+ *
+ * More Info:
+ *  http://ejohn.org/projects/javascript-diff-algorithm/
+ */
+
+function escape(s) {
+    var n = s;
+    n = n.replace(/&/g, "&amp;");
+    n = n.replace(/</g, "&lt;");
+    n = n.replace(/>/g, "&gt;");
+    n = n.replace(/"/g, "&quot;");
+
+    return n;
+}
+
+function diffString( o, n ) {
+  o = o.replace(/\s+$/, '');
+  n = n.replace(/\s+$/, '');
+
+  var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/) );
+  var str = "";
+
+  var oSpace = o.match(/\s+/g);
+  if (oSpace == null) {
+    oSpace = ["\n"];
+  } else {
+    oSpace.push("\n");
+  }
+  var nSpace = n.match(/\s+/g);
+  if (nSpace == null) {
+    nSpace = ["\n"];
+  } else {
+    nSpace.push("\n");
+  }
+
+  if (out.n.length == 0) {
+      for (var i = 0; i < out.o.length; i++) {
+        str += '<del>' + escape(out.o[i]) + oSpace[i] + "</del>";
+      }
+  } else {
+    if (out.n[0].text == null) {
+      for (n = 0; n < out.o.length && out.o[n].text == null; n++) {
+        str += '<del>' + escape(out.o[n]) + oSpace[n] + "</del>";
+      }
+    }
+
+    for ( var i = 0; i < out.n.length; i++ ) {
+      if (out.n[i].text == null) {
+        str += '<ins>' + escape(out.n[i]) + nSpace[i] + "</ins>";
+      } else {
+        var pre = "";
+
+        for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++ ) {
+          pre += '<del>' + escape(out.o[n]) + oSpace[n] + "</del>";
+        }
+        str += " " + out.n[i].text + nSpace[i] + pre;
+      }
+    }
+  }
+
+  return str;
+}
+
+function randomColor() {
+    return "rgb(" + (Math.random() * 100) + "%, " +
+                    (Math.random() * 100) + "%, " +
+                    (Math.random() * 100) + "%)";
+}
+function diffString2( o, n ) {
+  o = o.replace(/\s+$/, '');
+  n = n.replace(/\s+$/, '');
+
+  var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/) );
+
+  var oSpace = o.match(/\s+/g);
+  if (oSpace == null) {
+    oSpace = ["\n"];
+  } else {
+    oSpace.push("\n");
+  }
+  var nSpace = n.match(/\s+/g);
+  if (nSpace == null) {
+    nSpace = ["\n"];
+  } else {
+    nSpace.push("\n");
+  }
+
+  var os = "";
+  var colors = new Array();
+  for (var i = 0; i < out.o.length; i++) {
+      colors[i] = randomColor();
+
+      if (out.o[i].text != null) {
+          os += '<span style="background-color: ' +colors[i]+ '">' +
+                escape(out.o[i].text) + oSpace[i] + "</span>";
+      } else {
+          os += "<del>" + escape(out.o[i]) + oSpace[i] + "</del>";
+      }
+  }
+
+  var ns = "";
+  for (var i = 0; i < out.n.length; i++) {
+      if (out.n[i].text != null) {
+          ns += '<span style="background-color: ' +colors[out.n[i].row]+ '">' +
+                escape(out.n[i].text) + nSpace[i] + "</span>";
+      } else {
+          ns += "<ins>" + escape(out.n[i]) + nSpace[i] + "</ins>";
+      }
+  }
+
+  return { o : os , n : ns };
+}
+
+function diff( o, n ) {
+  var ns = new Object();
+  var os = new Object();
+
+  for ( var i = 0; i < n.length; i++ ) {
+    if ( ns[ n[i] ] == null )
+      ns[ n[i] ] = { rows: new Array(), o: null };
+    ns[ n[i] ].rows.push( i );
+  }
+
+  for ( var i = 0; i < o.length; i++ ) {
+    if ( os[ o[i] ] == null )
+      os[ o[i] ] = { rows: new Array(), n: null };
+    os[ o[i] ].rows.push( i );
+  }
+
+  for ( var i in ns ) {
+    if ( ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1 ) {
+      n[ ns[i].rows[0] ] = { text: n[ ns[i].rows[0] ], row: os[i].rows[0] };
+      o[ os[i].rows[0] ] = { text: o[ os[i].rows[0] ], row: ns[i].rows[0] };
+    }
+  }
+
+  for ( var i = 0; i < n.length - 1; i++ ) {
+    if ( n[i].text != null && n[i+1].text == null && n[i].row + 1 < o.length && o[ n[i].row + 1 ].text == null &&
+         n[i+1] == o[ n[i].row + 1 ] ) {
+      n[i+1] = { text: n[i+1], row: n[i].row + 1 };
+      o[n[i].row+1] = { text: o[n[i].row+1], row: i + 1 };
+    }
+  }
+
+  for ( var i = n.length - 1; i > 0; i-- ) {
+    if ( n[i].text != null && n[i-1].text == null && n[i].row > 0 && o[ n[i].row - 1 ].text == null &&
+         n[i-1] == o[ n[i].row - 1 ] ) {
+      n[i-1] = { text: n[i-1], row: n[i].row - 1 };
+      o[n[i].row-1] = { text: o[n[i].row-1], row: i - 1 };
+    }
+  }
+
+  return { o: o, n: n };
+}
diff --git a/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js
new file mode 100644
index 0000000..9420f1a
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/lib/jsdiff/jsdiff.min.js
@@ -0,0 +1,10 @@
+/*
+ * Javascript Diff Algorithm
+ *  By John Resig (http://ejohn.org/)
+ *  Modified by Chu Alan "sprite"
+ *
+ * Released under the MIT license.
+ *
+ * More Info:
+ *  http://ejohn.org/projects/javascript-diff-algorithm/
+ */function escape(a){var b=a;return b=b.replace(/&/g,"&amp;"),b=b.replace(/</g,"&lt;"),b=b.replace(/>/g,"&gt;"),b=b.replace(/"/g,"&quot;"),b}function diffString(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d="",e=a.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f=b.match(/\s+/g);f==null?f=["\n"]:f.push("\n");if(c.n.length==0)for(var g=0;g<c.o.length;g++)d+="<del>"+escape(c.o[g])+e[g]+"</del>";else{if(c.n[0].text==null)for(b=0;b<c.o.length&&c.o[b].text==null;b++)d+="<del>"+escape(c.o[b])+e[b]+"</del>";for(var g=0;g<c.n.length;g++)if(c.n[g].text==null)d+="<ins>"+escape(c.n[g])+f[g]+"</ins>";else{var h="";for(b=c.n[g].row+1;b<c.o.length&&c.o[b].text==null;b++)h+="<del>"+escape(c.o[b])+e[b]+"</del>";d+=" "+c.n[g].text+f[g]+h}}return d}function randomColor(){return"rgb("+Math.random()*100+"%, "+Math.random()*100+"%, "+Math.random()*100+"%)"}function diffString2(a,b){a=a.replace(/\s+$/,""),b=b.replace(/\s+$/,"");var c=diff(a==""?[]:a.split(/\s+/),b==""?[]:b.split(/\s+/)),d=a.match(/\s+/g);d==null?d=["\n"]:d.push("\n");var e=b.match(/\s+/g);e==null?e=["\n"]:e.push("\n");var f="",g=new Array;for(var h=0;h<c.o.length;h++)g[h]=randomColor(),c.o[h].text!=null?f+='<span style="background-color: '+g[h]+'">'+escape(c.o[h].text)+d[h]+"</span>":f+="<del>"+escape(c.o[h])+d[h]+"</del>";var i="";for(var h=0;h<c.n.length;h++)c.n[h].text!=null?i+='<span style="background-color: '+g[c.n[h].row]+'">'+escape(c.n[h].text)+e[h]+"</span>":i+="<ins>"+escape(c.n[h])+e[h]+"</ins>";return{o:f,n:i}}function diff(a,b){var c=new Object,d=new Object;for(var e=0;e<b.length;e++)c[b[e]]==null&&(c[b[e]]={rows:new Array,o:null}),c[b[e]].rows.push(e);for(var e=0;e<a.length;e++)d[a[e]]==null&&(d[a[e]]={rows:new Array,n:null}),d[a[e]].rows.push(e);for(var e in c)c[e].rows.length==1&&typeof d[e]!="undefined"&&d[e].rows.length==1&&(b[c[e].rows[0]]={text:b[c[e].rows[0]],row:d[e].rows[0]},a[d[e].rows[0]]={text:a[d[e].rows[0]],row:c[e].rows[0]});for(var e=0;e<b.length-1;e++)b[e].text!=null&&b[e+1].text==null&&b[e].row+1<a.length&&a[b[e].row+1].text==null&&b[e+1]==a[b[e].row+1]&&(b[e+1]={text:b[e+1],row:b[e].row+1},a[b[e].row+1]={text:a[b[e].row+1],row:e+1});for(var e=b.length-1;e>0;e--)b[e].text!=null&&b[e-1].text==null&&b[e].row>0&&a[b[e].row-1].text==null&&b[e-1]==a[b[e].row-1]&&(b[e-1]={text:b[e-1],row:b[e].row-1},a[b[e].row-1]={text:a[b[e].row-1],row:e-1});return{o:a,n:b}};
\ No newline at end of file
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt
index 6e87b37..1b09ac2 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/about.tt
@@ -653,6 +653,9 @@
             <h2>jQuery Colvis plugin</h2>
             <p>The <a href="http://datatables.net/extensions/colvis/">controls for column visiblity in DataTables</a>
                 by Allan Jardine is licensed under the BSD 3 and GPL v2 license.</p>
+
+            <h2>Javascript Diff Algorithm</h2>
+            <p>The <a href="http://ejohn.org/projects/javascript-diff-algorithm/">Javascript Diff Algorithm</a> plugin by John Resig is licensed under the <a href="http://opensource.org/licenses/mit-license.php">MIT License</a>.</p>
         </div>
 
         <div id="translations">
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/showdiffmarc.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/showdiffmarc.tt
index 2d33c84..93edb39 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/tools/showdiffmarc.tt
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/tools/showdiffmarc.tt
@@ -1,50 +1,55 @@
 [% INCLUDE 'doc-head-open.inc' %]
-<title>Koha -- Cataloging: MARC Import</title>
-
-<style>
-div#main {
-    width: 100%;
-    margin: 10px auto;
-}
-
-div.col1,
-div.col2 {
-    float: left;
-    width: 46%;
-    margin-left: 3%;
-
-}
-
-pre {
-    padding: 10px;
-    overflow: scroll;
-}
+<title>Koha &rsaquo; Tools &rsaquo; Manage staged MARC records &rsaquo; Compare matched records</title>
+[% INCLUDE 'doc-head-close.inc' %]
+<script type="text/javascript" src="[% interface %]/lib/jsdiff/jsdiff.min.js"></script>
+<script type="text/javascript">
+    $(document).ready(function(){
+      var diff1 = $("#col1 pre").text();
+      var diff2 = $("#col2 pre").text();
+      var diffs = diffString(diff1,diff2);
+      $("#col1 pre,#col2 pre").html(diffs);
+    });
+</script>
+<style type="text/css">
+    ins { background-color: #e6ffe6; }
+    del { background-color: #ffe6e6; }
+    #col1 ins, #col2 del { display: none; }
+    pre { padding: 10px; overflow: scroll; }
 </style>
+</head>
+<body id="tools_compare-marc-import" class="tools">
 
-[% INCLUDE 'doc-head-close.inc' %]
+[% INCLUDE 'header.inc' %]
 
-</head>
-<body>
+<div id="breadcrumbs">
+    <a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/tools/tools-home.pl">Tools</a> &rsaquo; <a href="/cgi-bin/koha/tools/manage-marc-import.pl">Manage staged MARC records</a> &rsaquo; <a href="/cgi-bin/koha/tools/manage-marc-import.pl?import_batch_id=[% batchid %]">Batch [% batchid %]</a> &rsaquo; Compare matched records
+</div>
 
-<div id="main">
-    <div class="col1">
-        <h2>Original</h2>
-        [% IF ( ERROR_FORMATTED1 ) %]
-            <p>The biblionumber <em>[% BIBLIONUMBER %]</em> doesn't match to any record.</p>
-        [% ELSE %]
-            <h1>[% BIBLIOTITLE %]</h1>
-            <pre>[% MARC_FORMATTED1 %]</pre>
-        [% END %]
+<div id="doc3">
+    <div class="yui-g">
+        <div id="col1" class="yui-u first">
+            <h1>Original</h1>
+            [% IF ( ERROR_FORMATTED1 ) %]
+                <div class="dialog alert">
+                    <p>The biblionumber <em>[% BIBLIONUMBER %]</em> doesn't match any existing record.</p>
+                </div>
+            [% ELSE %]
+                <h2>[% BIBLIOTITLE %]</h2>
+                <pre>[% MARC_FORMATTED1 %]</pre>
+            [% END %]
+        </div>
+        <div id="col2" class="yui-u">
+            <h1>Imported</h1>
+            [% IF ( ERROR_FORMATTED2 ) %]
+                <div class="dialog alert">
+                    <p>The import id number <em>[% IMPORTID %]</em> doesn't match any existing record.</p>
+                </div>
+            [% ELSE %]
+                <h2>[% IMPORTTITLE %]</h2>
+                <pre>[% MARC_FORMATTED2 %] </pre>
+            [% END %]
+        </div>
     </div>
-    <div class="col2">
-        <h2>Imported</h2>
-        [% IF ( ERROR_FORMATTED2 ) %]
-            <p>The importid <em>[% IMPORTID %]</em> doesn't match to any record.</p>
-        [% ELSE %]
-            <h1>[% IMPORTTITLE %]</h1>
-            <pre>[% MARC_FORMATTED2 %] </pre>
-        [% END %]
-    </div>
-</div>
 
+<p><a href="/cgi-bin/koha/tools/manage-marc-import.pl?import_batch_id=[% batchid %]">Return to staged MARC batch [% batchid %]</a></p>
 [% INCLUDE 'intranet-bottom.inc' %]
diff --git a/tools/batch_records_ajax.pl b/tools/batch_records_ajax.pl
index 57d7bc0..e3dec83 100755
--- a/tools/batch_records_ajax.pl
+++ b/tools/batch_records_ajax.pl
@@ -107,7 +107,7 @@ foreach my $record (@$records) {
           || q{},
         score => $#$match > -1 ? $match->[0]->{'score'} : 0,
         match_id => $match_id,
-        diff_url => $match_id ? "/cgi-bin/koha/tools/showdiffmarc.pl?importid=$record->{import_record_id}&id=$match_id" : undef
+        diff_url => $match_id ? "/cgi-bin/koha/tools/showdiffmarc.pl?batchid=$import_batch_id&importid=$record->{import_record_id}&id=$match_id" : undef
       };
 }
 
diff --git a/tools/showdiffmarc.pl b/tools/showdiffmarc.pl
index e0e0f7c..3b12795 100755
--- a/tools/showdiffmarc.pl
+++ b/tools/showdiffmarc.pl
@@ -8,7 +8,7 @@
 #
 # Koha is free software; you can redistribute it and/or modify it under the
 # terms of the GNU General Public License as published by the Free Software
-# Foundation; either version 2 of the License, or (at your option) any later
+# Foundation; either version 3 of the License, or (at your option) any later
 # version.
 #
 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
@@ -40,6 +40,7 @@ use XML::LibXML;
 my $input        = new CGI;
 my $biblionumber = $input->param('id');
 my $importid     = $input->param('importid');
+my $batchid      = $input->param('batchid');
 
 
 if ( $biblionumber and $importid ) {
@@ -56,7 +57,7 @@ if ( $biblionumber and $importid ) {
             query           => $input,
             type            => "intranet",
             authnotrequired => 0,
-            flagsrequired   => { catalogue => 1  },
+            flagsrequired   => { tools => 'manage_staged_marc' },
             debug           => 1,
         }
     );
@@ -82,7 +83,7 @@ if ( $biblionumber and $importid ) {
     }
 
 
-    $template->param(     SCRIPT_NAME => $ENV{'SCRIPT_NAME'},
+    $template->param(   SCRIPT_NAME => $ENV{'SCRIPT_NAME'},
                         BIBLIONUMBER => $biblionumber,
                         IMPORTID => $importid,
                         BIBLIOTITLE => $biblioTitle,
@@ -90,7 +91,8 @@ if ( $biblionumber and $importid ) {
                         MARC_FORMATTED1 => $formatted1,
                         MARC_FORMATTED2 => $formatted2,
                         ERROR_FORMATTED1 => $errorFormatted1,
-                        ERROR_FORMATTED2 => $errorFormatted2
+                        ERROR_FORMATTED2 => $errorFormatted2,
+                        batchid => $batchid
                     );
 
     output_html_with_http_headers $input, $cookie, $template->output;
-- 
1.7.2.5