Bug 26145: Add the ability to upload a cover image per item

This patchset adds the ability to attach cover images at item level.
This commit message will explain the different patches that are
following.

The main idea is to have cover images for a given item. This is useful
for a bibliographic record linked with a subscription. Each item could
have the cover image for the serial numbers.

In this first patch there is a limitation to allow only 1 cover per
item, but a later patch will remove it. That way we will take advantage
of the recent work done to display nicely cover images (bug 25031), and
reuse it in this development (staff interface only).

In order to use a flexible and robust code, the legacy C4 code
(C4::Images) has been moved to Koha::CoverImages. Also the DB table
biblioimages has been renamed cover_images.

Test plan (for the whole patch set):
0. Turn off AllowMultipleCovers
1. Create a new bibliographic record and items
2. Attach a cover image to the bibliographic record
3. In the item list of the bibliographic detail page, you will notice a
new "Upload image" action. Select it
4. Select an image
=> Notice the new column in the item table
5. Upload another image
=> You cannot upload 2 images, you are going to replace the existing one
6. Turn on AllowMultipleCovers
7. Attach another image to the image
=> Notice the 2 images are displayed nicely, with navigation controls
8. Confirm you can view an image if you click on it and you can delete it
9. Test the OPAC view now
=> Cover image for items are displayed in the table, there is no
navigation controls here however.

Sponsored-by: Gerhard Sondermann Dialog e.K. (presseplus.de, presseshop.at, presseshop.ch)

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>

Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
This commit is contained in:
Jonathan Druart 2020-07-28 21:18:20 +02:00
parent ff17013a65
commit c3f2993bdb
8 changed files with 147 additions and 49 deletions

View file

@ -18,12 +18,11 @@ package C4::Images;
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use strict;
use warnings;
use 5.010;
use Modern::Perl;
use C4::Context;
use GD;
use Koha::Exceptions;
use vars qw($debug $noimage @ISA @EXPORT);
@ -47,18 +46,32 @@ BEGIN {
=head2 PutImage
PutImage($biblionumber, $srcimage, $replace);
PutImage({ biblionumber => $biblionumber, itemnumber => $itemnumber, src_image => $srcimage, replace => $replace });
Stores binary image data and thumbnail in database, optionally replacing existing images for the given biblio.
Stores binary image data and thumbnail in database, optionally replacing existing images for the given biblio or item.
=cut
sub PutImage {
my ( $biblionumber, $srcimage, $replace ) = @_;
my ( $params ) = @_;
my $biblionumber = $params->{biblionumber};
my $itemnumber = $params->{itemnumber};
my $srcimage = $params->{src_image};
my $replace = $params->{replace};
Koha::Exceptions::WrongParameter->throw(
'PutImage cannot be called with both biblionumber and itemnumber')
if $biblionumber and $itemnumber;
Koha::Exceptions::WrongParameter->throw(
'PutImage must be called with "replace" if itemnumber is passed. Only 1 cover per item is allowed.')
if $itemnumber and not $replace;
return -1 unless defined($srcimage);
if ($replace) {
if ($biblionumber && $replace) {
foreach ( ListImagesForBiblio($biblionumber) ) {
DelImage($_);
}
@ -66,7 +79,7 @@ sub PutImage {
my $dbh = C4::Context->dbh;
my $query =
"INSERT INTO biblioimages (biblionumber, mimetype, imagefile, thumbnail) VALUES (?,?,?,?);";
"INSERT INTO biblioimages (biblionumber, itemnumber, mimetype, imagefile, thumbnail) VALUES (?,?,?,?,?);";
my $sth = $dbh->prepare($query);
my $mimetype = 'image/png'
@ -79,10 +92,10 @@ sub PutImage {
; # MAX pixel dims are 600 X 800 for full-size image...
$debug and warn "thumbnail is " . length($thumbnail) . " bytes.";
$sth->execute( $biblionumber, $mimetype, $fullsize->png(),
$sth->execute( $biblionumber, $itemnumber, $mimetype, $fullsize->png(),
$thumbnail->png() );
my $dberror = $sth->errstr;
warn "Error returned inserting $biblionumber.$mimetype." if $sth->errstr;
warn sprintf("Error returned inserting %s.%s.", ($biblionumber || $itemnumber, $mimetype)) if $sth->errstr;
undef $thumbnail;
undef $fullsize;
return $dberror;
@ -135,6 +148,22 @@ sub ListImagesForBiblio {
return @imagenumbers;
}
=head2 GetImageForItem
my $image = GetImageForItem($itemnumber);
Gets the image associated with a particular item.
=cut
sub GetImageForItem {
my ($itemnumber) = @_;
my $dbh = C4::Context->dbh;
return $dbh->selectrow_array(
'SELECT imagenumber FROM biblioimages WHERE itemnumber = ?',
undef, $itemnumber );
}
=head2 DelImage
my ($dberror) = DelImage($imagenumber);

View file

@ -322,6 +322,7 @@ if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
$template->param(SeparateHoldings => 1);
}
my $separatebranch = C4::Context->preference('SeparateHoldingsBranch') || 'homebranch';
my ( $itemloop_has_images, $otheritemloop_has_images );
foreach my $item (@items) {
my $itembranchcode = $item->{$separatebranch};
@ -406,17 +407,30 @@ foreach my $item (@items) {
}
}
if ( C4::Context->preference("LocalCoverImages") == 1 ) {
$item->{imagenumber} =
C4::Images::GetImageForItem( $item->{itemnumber} );
}
if ($currentbranch and C4::Context->preference('SeparateHoldings')) {
if ($itembranchcode and $itembranchcode eq $currentbranch) {
push @itemloop, $item;
$itemloop_has_images++ if $item->{imagenumber};
} else {
push @otheritemloop, $item;
$otheritemloop_has_images++ if $item->{imagenumber};
}
} else {
push @itemloop, $item;
$itemloop_has_images++ if $item->{imagenumber};
}
}
$template->param(
itemloop_has_images => $itemloop_has_images,
otheritemloop_has_images => $otheritemloop_has_images,
);
# Display only one tab if one items list is empty
if (scalar(@itemloop) == 0 || scalar(@otheritemloop) == 0) {
$template->param(SeparateHoldings => 0);

View file

@ -28,6 +28,7 @@ use C4::Images;
use C4::Search;
use Koha::Biblios;
use Koha::Items;
use Koha::Patrons;
my $query = new CGI;
@ -40,7 +41,8 @@ my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
}
);
my $biblionumber = $query->param('biblionumber') || $query->param('bib');
my $itemnumber = $query->param('itemnumber');
my $biblionumber = $query->param('biblionumber') || $query->param('bib') || Koha::Items->find($itemnumber)->biblionumber;
my $imagenumber = $query->param('imagenumber');
my $biblio = Koha::Biblios->find( $biblionumber );
my $itemcount = $biblio ? $biblio->items->count : 0;
@ -65,10 +67,22 @@ if( $query->cookie("searchToOrder") ){
}
if ( C4::Context->preference("LocalCoverImages") ) {
my @images = ListImagesForBiblio($biblionumber);
$template->{VARS}->{'LocalCoverImages'} = 1;
$template->{VARS}->{'images'} = \@images;
$template->{VARS}->{'imagenumber'} = $imagenumber || $images[0] || '';
if ( $itemnumber ) {
my $image = C4::Images::GetImageForItem($itemnumber);
$template->param(
LocalCoverImages => 1,
images => [$image],
imagenumber => $imagenumber,
);
} else {
my @images = ListImagesForBiblio($biblionumber);
$template->param(
LocalCoverImages => 1,
images => \@images,
imagenumber => $imagenumber || $images[0] || '',
);
}
}
$template->{VARS}->{'count'} = $itemcount;
$template->{VARS}->{'biblionumber'} = $biblionumber;

View file

@ -285,6 +285,9 @@
<thead>
<tr>
[% IF (StaffDetailItemSelection) %]<th id="[% tab | html %]_checkbox" data-colname="[% tab | html %]_checkbox" class="NoSort"></th>[% END %]
[% IF ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
<th id="[% tab | html %]_cover" data-colname="[% tab | html %]_cover">Cover image</th>
[% END %]
[% IF ( item_level_itypes ) %]<th id="[% tab | html %]_itype" data-colname="[% tab | html %]_itype">Item type</th>[% END %]
<th id="[% tab | html %]_holdingbranch" data-colname="[% tab | html %]_holdingbranch">Current library</th>
<th id="[% tab | html %]_homebranch" data-colname="[% tab | html %]_homebranch">Home library</th>
@ -323,6 +326,16 @@
<input type="checkbox" value="[% item.itemnumber | html %]" name="itemnumber" />
</td>
[% END %]
[% IF ( tab == 'holdings' && itemloop_has_images || tab == 'otherholdings' && otheritemloop_has_images ) %]
<td class="cover">
[% IF item.imagenumber %]
<a href="/cgi-bin/koha/catalogue/imageviewer.pl?itemnumber=[% item.itemnumber | uri %]&amp;imagenumber=[% item.imagenumber | uri %]">
<img src="/cgi-bin/koha/catalogue/image.pl?thumbnail=1amp;imagenumber=[% item.imagenumber | uri %]" />
</a>
[% END %]
</td>
[% END %]
[% IF ( item_level_itypes ) %]
<td class="itype">
[% IF !noItemTypeImages && item.imageurl %]
@ -533,7 +546,12 @@ Note that permanent location is a code, and location may be an authval.
[% IF CAN_user_editcatalogue_edit_items %]
<td class="actions">
[% UNLESS item.cannot_be_edited %]
<a class="btn btn-default btn-xs" href="/cgi-bin/koha/cataloguing/additem.pl?op=edititem&biblionumber=[% item.biblionumber | html %]&itemnumber=[% item.itemnumber | html %]#edititem"><i class="fa fa-pencil"></i> Edit</a>
<div class="btn-group">
<a class="btn btn-default btn-xs" href="/cgi-bin/koha/cataloguing/additem.pl?op=edititem&biblionumber=[% item.biblionumber | html %]&itemnumber=[% item.itemnumber | html %]#edititem"><i class="fa fa-pencil"></i> Edit</a><a class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown"><span class="caret"></span></a>
<ul class="dropdown-menu pull-right">
<li><a href="/cgi-bin/koha/tools/upload-cover-image.pl?itemnumber=[% item.itemnumber | uri %]&amp;filetype=image"><i class="fa fa-upload">Upload image</i></a></li>
</ul>
</div>
[% END %]
</td>
[% END %]
@ -541,6 +559,7 @@ Note that permanent location is a code, and location may be an authval.
[% END %]
</tbody>
</table>
[% END %][%# end of block items_table %]
<div id="holdings">
@ -1054,7 +1073,7 @@ Note that permanent location is a code, and location may be an authval.
thumbnail.find("img").css("opacity", ".2");
thumbnail.find("a.remove").html("<img style='display:inline-block' src='" + interface + "/" + theme + "/img/spinner-small.gif' alt='' />");
$.ajax({
url: "/cgi-bin/koha/svc/cover_images?action=delete&biblionumber=" + biblionumber + "&imagenumber=" + imagenumber,
url: "/cgi-bin/koha/svc/cover_images?action=delete&imagenumber=" + imagenumber,
success: function(data) {
$(data).each( function(i) {
if ( this.deleted == 1 ) {

View file

@ -133,7 +133,7 @@
thumbnail.find("a.remove").html("<img style='display:inline-block' src='" + interface + "/" + theme + "/img/spinner-small.gif' alt='' />");
$.ajax({
url: "/cgi-bin/koha/svc/cover_images?action=delete&biblionumber=" + biblionumber + "&imagenumber=" + imagenumber,
url: "/cgi-bin/koha/svc/cover_images?action=delete&imagenumber=" + imagenumber,
success: function(data) {
$(data).each( function() {
if ( this.deleted == 1 ) {

View file

@ -77,24 +77,34 @@
<fieldset class="rows">
<legend>File type</legend>
<ol>
<li class="radio">
[% IF (filetype != 'image' ) %]<input type="radio" id="zipfile" name="filetype" value="zip" checked="checked" />[% ELSE %]<input type="radio" id="zipfile" name="filetype" value="zip" />[% END %]
<label for="zipfile">ZIP file</label>
</li>
<li class="radio">
[% IF (filetype == 'image' ) %]<input type="radio" id="image" name="filetype" value="image" checked="checked" />[% ELSE %]<input type="radio" id="image" name="filetype" value="image" />[% END %]
<label for="image">Image file</label>
</li>
<li class="radio">
[% IF ( filetype == 'image' ) %]<span id="bibnum">[% ELSE %]<span id="bibnum" style="display: none">[% END %]<label for="biblionumber">Enter cover biblionumber: </label><input type="text" id="biblionumber" name="biblionumber" value="[% biblionumber | html %]" size="15" /></span>
</li>
[% UNLESS itemnumber %]
<li class="radio">
[% IF (filetype != 'image' ) %]<input type="radio" id="zipfile" name="filetype" value="zip" checked="checked" />[% ELSE %]<input type="radio" id="zipfile" name="filetype" value="zip" />[% END %]
<label for="zipfile">ZIP file</label>
</li>
<li class="radio">
[% IF (filetype == 'image' ) %]<input type="radio" id="image" name="filetype" value="image" checked="checked" />[% ELSE %]<input type="radio" id="image" name="filetype" value="image" />[% END %]
<label for="image">Image file</label>
</li>
<li class="radio">
[% IF ( filetype == 'image' ) %]<span id="bibnum">[% ELSE %]<span id="bibnum" style="display: none">[% END %]<label for="biblionumber">Enter cover biblionumber: </label><input type="text" id="biblionumber" name="biblionumber" value="[% biblionumber | html %]" size="15" /></span>
</li>
[% ELSE %]
<label for="itemnumber">Cover itemnumber: </label>
<input type="text" id="itemnumber" name="itemnumber" value="[% itemnumber | html %]" size="15" readonly="readonly" />
<input type="hidden" name="filetype" value="image" />
[% END %]
</ol>
</fieldset>
<fieldset class="rows">
<legend>Options</legend>
<ol>
<li class="checkbox">
[% IF AllowMultipleCovers == 0 %]<input type="checkbox" id="replace" name="replace" checked="checked" disabled="disabled" value="1" />[% ELSE %]<input type="checkbox" id="replace" name="replace" value="1" />[% END %]
[% IF AllowMultipleCovers == 0 OR itemnumber%]
<input type="checkbox" id="replace" name="replace" checked="checked" disabled="disabled" value="1" />
[% ELSE %]
<input type="checkbox" id="replace" name="replace" value="1" />
[% END %]
<label for="replace">Replace existing covers</label>
</li>
</ol>

View file

@ -37,29 +37,28 @@ if ( $auth_status ne "ok" ) {
}
my $action = $input->param('action');
my $biblionumber = $input->param('biblionumber');
my @imagenumbers = $input->param('imagenumber');
# Array to store the reponse JSON
my $response = [];
if ( $action eq "delete" ) {
# Build a hash of valid imagenumbers fr the given biblionumber
my %valid_imagenumbers = map {$_ => 1} ListImagesForBiblio($biblionumber);
foreach my $imagenumber ( @imagenumbers ) {
if ( exists( $valid_imagenumbers{ $imagenumber } ) ) {
eval {
DelImage($imagenumber);
push @$response, {
imagenumber => $imagenumber,
deleted => 1
};
} else {
};
if ( $@ ) {
push @$response, {
imagenumber => $imagenumber,
deleted => 0,
error => "MSG_INVALID_IMAGENUMBER"
};
} else {
push @$response, {
imagenumber => $imagenumber,
deleted => 1
};
}
}
} else {

View file

@ -46,6 +46,7 @@ use C4::Context;
use C4::Auth;
use C4::Output;
use C4::Images;
use Koha::Items;
use Koha::UploadedFiles;
use C4::Log;
@ -66,6 +67,7 @@ my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
my $filetype = $input->param('filetype');
my $biblionumber = $input->param('biblionumber');
my $itemnumber = $input->param('itemnumber');
#my $uploadfilename = $input->param('uploadfile'); # obsolete?
my $replace = !C4::Context->preference("AllowMultipleCovers")
|| $input->param('replace');
@ -75,8 +77,11 @@ my $sessionID = $cookies{'CGISESSID'}->value;
my $error;
$template->{VARS}->{'filetype'} = $filetype;
$template->{VARS}->{'biblionumber'} = $biblionumber;
$template->param(
filetype => $filetype,
biblionumber => $biblionumber,
itemnumber => $itemnumber,
);
my $total = 0;
@ -87,7 +92,7 @@ if ($fileID) {
my $srcimage = GD::Image->new($fh);
$fh->close if $fh;
if ( defined $srcimage ) {
my $dberror = PutImage( $biblionumber, $srcimage, $replace );
my $dberror = PutImage( { biblionumber => $biblionumber, itemnumber => $itemnumber, src_image => $srcimage, replace => $replace } );
if ($dberror) {
$error = 'DBERR';
}
@ -157,9 +162,13 @@ if ($fileID) {
my $srcimage = GD::Image->new("$dir/$filename");
if ( defined $srcimage ) {
$total++;
my $dberror =
PutImage( $biblionumber, $srcimage,
$replace );
my $dberror = PutImage(
{
biblionumber => $biblionumber,
src_image => $srcimage,
replace => $replace
}
);
if ($dberror) {
$error = 'DBERR';
}
@ -178,10 +187,14 @@ if ($fileID) {
}
}
}
$template->{VARS}->{'total'} = $total;
$template->{VARS}->{'uploadimage'} = 1;
$template->{VARS}->{'error'} = $error;
$template->{VARS}->{'biblionumber'} = $biblionumber;
$template->param(
total => $total,
uploadimage => 1,
error => $error,
biblionumber => $biblionumber || Koha::Items->find($itemnumber)->biblionumber,
itemnumber => $itemnumber,
);
}
output_html_with_http_headers $input, $cookie, $template->output;