View | Details | Raw Unified | Return to bug 3050
Collapse All | Expand All

(-)a/Koha/Misc/Files.pm (+266 lines)
Line 0 Link Here
1
package Koha::Misc::Files;
2
3
# This file is part of Koha.
4
#
5
# Copyright 2012 Kyle M Hall
6
# Copyright 2014 Jacek Ablewicz
7
# Based on Koha/Borrower/Files.pm by Kyle M Hall
8
#
9
# Koha is free software; you can redistribute it and/or modify it
10
# under the terms of the GNU General Public License as published by
11
# the Free Software Foundation; either version 3 of the License, or
12
# (at your option) any later version.
13
#
14
# Koha is distributed in the hope that it will be useful, but
15
# WITHOUT ANY WARRANTY; without even the implied warranty of
16
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
# GNU General Public License for more details.
18
#
19
# You should have received a copy of the GNU General Public License
20
# along with Koha; if not, see <http://www.gnu.org/licenses>.
21
22
use Modern::Perl;
23
use vars qw($VERSION);
24
$VERSION = '0.25';
25
26
use C4::Context;
27
use C4::Output;
28
use C4::Dates;
29
30
=head1 NAME
31
32
Koha::Misc::Files - module for managing miscellaneous files associated
33
with records from arbitrary tables
34
35
=head1 SYNOPSIS
36
37
use Koha::Misc::Files;
38
39
my $mf = Koha::Misc::Files->new( tabletag => $tablename,
40
    recordid => $recordnumber );
41
42
=head1 FUNCTIONS
43
44
=over
45
46
=item new()
47
48
my $mf = Koha::Misc::Files->new( tabletag => $tablename,
49
    recordid => $recordnumber );
50
51
Creates new Koha::Misc::Files object. Such object is essentially
52
a pair: in typical usage scenario, 'tabletag' parameter will be
53
a database table name, and 'recordid' an unique record ID number
54
from this table. However, this method does accept an arbitrary
55
string as 'tabletag', and an arbitrary integer as 'recordid'.
56
57
Particular Koha::Misc::Files object can have one or more file records
58
(actuall file contents + various file metadata) associated with it.
59
60
In case of an error (wrong parameter format) it returns undef.
61
62
=cut
63
64
sub new {
65
    my ( $class, %args ) = @_;
66
67
    my $recid = $args{'recordid'};
68
    my $tag   = $args{'tabletag'};
69
    ( defined($tag) && $tag ne '' && defined($recid) && $recid =~ /^\d+$/ )
70
      || return ();
71
72
    my $self = bless( {}, $class );
73
74
    $self->{'table_tag'} = $tag;
75
    $self->{'record_id'} = '' . ( 0 + $recid );
76
77
    return $self;
78
}
79
80
=item GetFilesInfo()
81
82
my $files_descriptions = $mf->GetFilesInfo();
83
84
This method returns a reference to an array of hashes
85
containing files metadata (file_id, file_name, file_type,
86
file_description, file_size, date_uploaded) for all file records
87
associated with given $mf object, or an empty arrayref if there are
88
no such records yet.
89
90
In case of an error it returns undef.
91
92
=cut
93
94
sub GetFilesInfo {
95
    my $self = shift;
96
97
    my $dbh   = C4::Context->dbh;
98
    my $query = '
99
        SELECT
100
            file_id,
101
            file_name,
102
            file_type,
103
            file_description,
104
            date_uploaded,
105
            LENGTH(file_content) AS file_size
106
        FROM misc_files
107
        WHERE table_tag = ? AND record_id = ?
108
        ORDER BY file_name, date_uploaded
109
    ';
110
    my $sth = $dbh->prepare($query);
111
    $sth->execute( $self->{'table_tag'}, $self->{'record_id'} );
112
    return $sth->fetchall_arrayref( {} );
113
}
114
115
=item AddFile()
116
117
$mf->AddFile( name => $filename, type => $mimetype,
118
    description => $description, content => $content );
119
120
Adds a new file (we want to store for / associate with a given
121
object) to the database. Parameters 'name' and 'content' are mandatory.
122
Note: this method would (silently) fail if there is no 'name' given
123
or if the 'content' provided is empty.
124
125
=cut
126
127
sub AddFile {
128
    my ( $self, %args ) = @_;
129
130
    my $name        = $args{'name'};
131
    my $type        = $args{'type'} // '';
132
    my $description = $args{'description'};
133
    my $content     = $args{'content'};
134
135
    return unless ( defined($name) && $name ne '' && defined($content) && $content ne '' );
136
137
    my $dbh   = C4::Context->dbh;
138
    my $query = '
139
        INSERT INTO misc_files ( table_tag, record_id, file_name, file_type, file_description, file_content )
140
        VALUES ( ?,?,?,?,?,? )
141
    ';
142
    my $sth = $dbh->prepare($query);
143
    $sth->execute( $self->{'table_tag'}, $self->{'record_id'}, $name, $type,
144
        $description, $content );
145
}
146
147
=item GetFile()
148
149
my $file = $mf->GetFile( id => $file_id );
150
151
For an individual, specific file ID this method returns a hashref
152
containing all metadata (file_id, table_tag, record_id, file_name,
153
file_type, file_description, file_content, date_uploaded), plus
154
an actuall contents of a file (in 'file_content'). In typical usage
155
scenarios, for a given $mf object, specific file IDs have to be
156
obtained first by GetFilesInfo() call.
157
158
Returns undef in case when file ID specified as 'id' parameter was not
159
found in the database.
160
161
=cut
162
163
sub GetFile {
164
    my ( $self, %args ) = @_;
165
166
    my $file_id = $args{'id'};
167
168
    my $dbh   = C4::Context->dbh;
169
    my $query = '
170
        SELECT * FROM misc_files WHERE file_id = ? AND table_tag = ? AND record_id = ?
171
    ';
172
    my $sth = $dbh->prepare($query);
173
    $sth->execute( $file_id, $self->{'table_tag'}, $self->{'record_id'} );
174
    return $sth->fetchrow_hashref();
175
}
176
177
=item DelFile()
178
179
$mf->DelFile( id => $file_id );
180
181
Deletes specific, individual file record (file contents and metadata)
182
from the database.
183
184
=cut
185
186
sub DelFile {
187
    my ( $self, %args ) = @_;
188
189
    my $file_id = $args{'id'};
190
191
    my $dbh   = C4::Context->dbh;
192
    my $query = '
193
        DELETE FROM misc_files WHERE file_id = ? AND table_tag = ? AND record_id = ?
194
    ';
195
    my $sth = $dbh->prepare($query);
196
    $sth->execute( $file_id, $self->{'table_tag'}, $self->{'record_id'} );
197
}
198
199
=item DelAllFiles()
200
201
$mf->DelAllFiles();
202
203
Deletes all file records associated with (stored for) a given $mf object.
204
205
=cut
206
207
sub DelAllFiles {
208
    my ($self) = @_;
209
210
    my $dbh   = C4::Context->dbh;
211
    my $query = '
212
        DELETE FROM misc_files WHERE table_tag = ? AND record_id = ?
213
    ';
214
    my $sth = $dbh->prepare($query);
215
    $sth->execute( $self->{'table_tag'}, $self->{'record_id'} );
216
}
217
218
=item MergeFileRecIds()
219
220
$mf->MergeFileRecIds(@ids_to_be_merged);
221
222
This method re-associates all individuall file records associated with
223
some "parent" records IDs (provided in @ids_to_be_merged) with the given
224
single $mf object (which would be treated as a "parent" destination).
225
226
This a helper method; typically it needs to be called only in cases when
227
some "parent" records are being merged in the (external) 'tablename'
228
table.
229
230
=cut
231
232
sub MergeFileRecIds {
233
    my ( $self, @ids_to_merge ) = @_;
234
235
    my $dst_recid = $self->{'record_id'};
236
    @ids_to_merge = map { ( $dst_recid == $_ ) ? () : ($_); } @ids_to_merge;
237
    @ids_to_merge > 0 || return ();
238
239
    my $dbh   = C4::Context->dbh;
240
    my $query = '
241
        UPDATE misc_files SET record_id = ?
242
        WHERE table_tag = ? AND record_id = ?
243
    ';
244
    my $sth = $dbh->prepare($query);
245
246
    for my $src_recid (@ids_to_merge) {
247
        $sth->execute( $dst_recid, $self->{'table_tag'}, $src_recid );
248
    }
249
}
250
251
1;
252
253
__END__
254
255
=back
256
257
=head1 SEE ALSO
258
259
Koha::Borrower::Files
260
261
=head1 AUTHOR
262
263
Kyle M Hall E<lt>kyle.m.hall@gmail.comE<gt>,
264
Jacek Ablewicz E<lt>ablewicz@gmail.comE<gt>
265
266
=cut
(-)a/acqui/invoice-files.pl (+121 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Copyright 2014 Jacek Ablewicz
6
#
7
# Koha is free software; you can redistribute it and/or modify it
8
# under the terms of the GNU General Public License as published by
9
# the Free Software Foundation; either version 3 of the License, or
10
# (at your option) any later version.
11
#
12
# Koha is distributed in the hope that it will be useful, but
13
# WITHOUT ANY WARRANTY; without even the implied warranty of
14
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
# GNU General Public License for more details.
16
#
17
# You should have received a copy of the GNU General Public License
18
# along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20
=head1 NAME
21
22
invoice-files.pl
23
24
=head1 DESCRIPTION
25
26
Manage files associated with invoice
27
28
=cut
29
30
use Modern::Perl;
31
32
use CGI;
33
use C4::Auth;
34
use C4::Output;
35
use C4::Acquisition;
36
use Koha::Misc::Files;
37
38
my $input = new CGI;
39
my ( $template, $loggedinuser, $cookie, $flags ) = get_template_and_user(
40
    {
41
        template_name   => 'acqui/invoice-files.tt',
42
        query           => $input,
43
        type            => 'intranet',
44
        authnotrequired => 0,
45
        flagsrequired   => { 'acquisition' => '*' },
46
        debug           => 1,
47
    }
48
);
49
50
my $invoiceid = $input->param('invoiceid') // '';
51
my $op = $input->param('op') // '';
52
my %errors;
53
54
my $mf = Koha::Misc::Files->new( tabletag => 'aqinvoices', recordid => $invoiceid );
55
defined($mf) || do { $op = 'none'; $errors{'invalid_parameter'} = 1; };
56
57
if ( $op eq 'download' ) {
58
    my $file_id = $input->param('file_id');
59
    my $file = $mf->GetFile( id => $file_id );
60
61
    my $fname = $file->{'file_name'};
62
    my $ftype = $file->{'file_type'};
63
    if ($input->param('view') && ($ftype =~ m|^image/|i || $fname =~ /\.pdf/i)) {
64
        $fname =~ /\.pdf/i && do { $ftype='application/pdf'; };
65
        print $input->header(
66
            -type       => $ftype,
67
            -charset    => 'utf-8'
68
        );
69
    } else {
70
        print $input->header(
71
            -type       => $file->{'file_type'},
72
            -charset    => 'utf-8',
73
            -attachment => $file->{'file_name'}
74
        );
75
    }
76
    print $file->{'file_content'};
77
}
78
else {
79
    my $details = GetInvoiceDetails($invoiceid);
80
    $template->param(
81
        invoiceid        => $details->{'invoiceid'},
82
        invoicenumber    => $details->{'invoicenumber'},
83
        suppliername     => $details->{'suppliername'},
84
        booksellerid     => $details->{'booksellerid'},
85
        datereceived     => $details->{'datereceived'},
86
    );
87
88
    if ( $op eq 'upload' ) {
89
        my $uploaded_file = $input->upload('uploadfile');
90
91
        if ($uploaded_file) {
92
            my $filename = $input->param('uploadfile');
93
            my $mimetype = $input->uploadInfo($filename)->{'Content-Type'};
94
95
            $errors{'empty_upload'} = 1 if ( -z $uploaded_file );
96
            unless (%errors) {
97
                my $file_content = do { local $/; <$uploaded_file>; };
98
                if ($mimetype =~ /^application\/(force-download|unknown)$/i && $filename =~ /\.pdf$/i) {
99
                    $mimetype = 'application/pdf';
100
                }
101
                $mf->AddFile(
102
                    name    => $filename,
103
                    type    => $mimetype,
104
                    content => $file_content,
105
                    description => $input->param('description')
106
                );
107
            }
108
        }
109
        else {
110
            $errors{'no_file'} = 1;
111
        }
112
    } elsif ( $op eq 'delete' ) {
113
        $mf->DelFile( id => $input->param('file_id') );
114
    }
115
116
    $template->param(
117
        files => (defined($mf)? $mf->GetFilesInfo(): undef),
118
        errors => \%errors
119
    );
120
    output_html_with_http_headers $input, $cookie, $template->output;
121
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/acqui/invoice-files.tt (+100 lines)
Line 0 Link Here
1
[% USE KohaDates %]
2
3
[% INCLUDE 'doc-head-open.inc' %]
4
<title>Koha &rsaquo; Acquisitions &rsaquo; Invoice &rsaquo; Files</title>
5
<link rel="stylesheet" type="text/css" href="[% themelang %]/css/datatables.css" />
6
[% INCLUDE 'doc-head-close.inc' %]
7
[% INCLUDE 'datatables.inc' %]
8
<script type="text/javascript">
9
//<![CDATA[
10
    $(document).ready(function() {
11
        $("#invoice_files_details_table").dataTable($.extend(true, {}, dataTablesDefaults, {
12
            "aoColumnDefs": [
13
                { "aTargets": [ -1, -2 ], "bSortable": false, "bSearchable": false },
14
                { "aTargets": [ 3 ], "sType": "natural" }
15
            ],
16
            bInfo: false,
17
            bPaginate: false,
18
            bFilter: false,
19
            sDom: "t"
20
        }));
21
    });
22
//]]>
23
</script>
24
</head>
25
<body>
26
[% INCLUDE 'header.inc' %]
27
[% INCLUDE 'acquisitions-search.inc' %]
28
29
<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/acqui/acqui-home.pl">Acquisitions</a> &rsaquo; <a href="/cgi-bin/koha/acqui/invoices.pl">Invoices</a> &rsaquo; <a href="/cgi-bin/koha/acqui/invoice.pl?invoiceid=[% invoiceid %]">[% invoicenumber %]</a> &rsaquo; Files</div>
30
31
<div id="doc3" class="yui-t2">
32
33
<div id="bd">
34
  <div id="yui-main">
35
    <div class="yui-b">
36
      <h2>Files for invoice: [% invoicenumber | html %]</h2>
37
      <p><b>Vendor: </b><a href="/cgi-bin/koha/acqui/supplier.pl?booksellerid=[% booksellerid %]">[% suppliername %]</a></p>
38
      <br />
39
      [% IF errors %]
40
        <div class="dialog alert">
41
          [% IF errors.empty_upload %]The file you are attempting to upload has no contents.[% END %]
42
          [% IF errors.no_file %]You did not select a file to upload.[% END %]
43
          [% IF errors.invalid_parameter %]Invalid or missing script parameter.[% END %]
44
        </div>
45
      [% END %]
46
      [% IF files %]
47
          <table id="invoice_files_details_table">
48
              <thead>
49
                  <tr>
50
                      <th>Name</th>
51
                      <th>Type</th>
52
                      <th>Description</th>
53
                      <th>Uploaded</th>
54
                      <th>Bytes</th>
55
                      <th>&nbsp;</th>
56
                      <th>&nbsp;</th>
57
                  </tr>
58
              </thead>
59
              <tbody>
60
                [% FOREACH f IN files %]
61
                  <tr>
62
                      <td><a href="?invoiceid=[% invoiceid %]&amp;op=download&amp;view=1&amp;file_id=[% f.file_id %]">[% f.file_name | html %]</a></td>
63
                      <td>[% f.file_type | html %]</td>
64
                      <td>[% f.file_description | html %]</td>
65
                      <td><!-- [% f.date_uploaded %] -->[% f.date_uploaded | $KohaDates %]</td>
66
                      <td>[% f.file_size %]</td>
67
                      <td><a href="?invoiceid=[% invoiceid %]&amp;op=delete&amp;file_id=[% f.file_id %]">Delete</a></td>
68
                      <td><a href="?invoiceid=[% invoiceid %]&amp;op=download&amp;file_id=[% f.file_id %]">Download</a></td>
69
                  </tr>
70
                [% END %]
71
              </tbody>
72
          </table>
73
      [% ELSE %]
74
          <div class="dialog message">
75
              <p>This invoice has no files attached.</p>
76
          </div>
77
      [% END %]
78
      [% IF invoiceid %]
79
          <br />
80
          <form method="post" action="/cgi-bin/koha/acqui/invoice-files.pl" enctype="multipart/form-data">
81
              <fieldset class="rows">
82
                  <legend>Upload New File</legend>
83
                  <ol>
84
                      <li><input type="hidden" name="op" value="upload" />
85
                      <input type="hidden" name="invoiceid" value="[% invoiceid %]" />
86
                      <label for="description">Description:</label>
87
                      <input name="description" id="description" type="text" /></li>
88
                      <li><label for="uploadfile">File:</label><input name="uploadfile" type="file" id="uploadfile" /></li>
89
                  </ol>
90
                  <fieldset class="action"><input name="upload" type="submit" id="upload" value="Upload File" /></fieldset>
91
              </fieldset>
92
          </form>
93
      [% END %]
94
    </div>
95
  </div>
96
  <div class="yui-b">
97
    [% INCLUDE 'acquisitions-menu.inc' %]
98
  </div>
99
</div>
100
[% INCLUDE 'intranet-bottom.inc' %]
(-)a/t/db_dependent/Koha_Misc_Files.t (-1 / +87 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# Unit tests for Koha::Misc::Files
4
# Author: Jacek Ablewicz, abl@biblos.pk.edu.pl
5
6
use Modern::Perl;
7
use C4::Context;
8
use Test::More tests => 27;
9
10
BEGIN {
11
    use_ok('Koha::Misc::Files');
12
}
13
14
my $dbh = C4::Context->dbh;
15
$dbh->{AutoCommit} = 0;
16
$dbh->{RaiseError} = 1;
17
18
## new() parameter handling check
19
is(Koha::Misc::Files->new(recordid => 12), undef, "new() param check test/1");
20
is(Koha::Misc::Files->new(recordid => 'aa123', tabletag => 'ttag_a'), undef, "new() param check test/2");
21
22
## create some test objects with arbitrary (tabletag, recordid) pairs
23
my $mf_a_123 = Koha::Misc::Files->new(recordid => '123', tabletag => 'tst_table_a');
24
my $mf_a_124 = Koha::Misc::Files->new(recordid => '124', tabletag => 'tst_table_a');
25
my $mf_b_221 = Koha::Misc::Files->new(recordid => '221', tabletag => 'tst_table_b');
26
is(ref($mf_a_123), "Koha::Misc::Files", "new() returned object type");
27
28
## GetFilesInfo() initial tests (dummy AddFile() / parameter handling checks)
29
is(ref($mf_a_123->GetFilesInfo()), 'ARRAY', "GetFilesInfo() return type");
30
is(scalar @{$mf_a_123->GetFilesInfo()}, 0, "GetFilesInfo() empty/non-empty result/1");
31
$mf_a_123->AddFile(name => '', type => 'text/plain', content => "aaabbcc");
32
is(scalar @{$mf_a_123->GetFilesInfo()}, 0, "GetFilesInfo() empty/non-empty result/2");
33
34
## AddFile(); add 5 sample file records for 3 test objects
35
$mf_a_123->AddFile(name => 'File_name_1.txt', type => 'text/plain',
36
  content => "file contents\n1111\n", description => "File #1 sample description");
37
$mf_a_123->AddFile(name => 'File_name_2.txt', type => 'text/plain',
38
  content => "file contents\n2222\n", description => "File #2 sample description");
39
$mf_a_124->AddFile(name => 'File_name_3.txt', content => "file contents\n3333\n", type => 'text/whatever');
40
$mf_a_124->AddFile(name => 'File_name_4.txt', content => "file contents\n4444\n");
41
$mf_b_221->AddFile(name => 'File_name_5.txt', content => "file contents\n5555\n");
42
43
## check GetFilesInfo() results for added files
44
my $files_a_123_infos = $mf_a_123->GetFilesInfo();
45
is(scalar @$files_a_123_infos, 2, "GetFilesInfo() result count/1");
46
is(scalar @{$mf_b_221->GetFilesInfo()}, 1, "GetFilesInfo() result count/2");
47
is(ref($files_a_123_infos->[0]), 'HASH', "GetFilesInfo() item file result type");
48
is($files_a_123_infos->[0]->{file_name}, 'File_name_1.txt', "GetFilesInfo() result check/1");
49
is($files_a_123_infos->[1]->{file_name}, 'File_name_2.txt', "GetFilesInfo() result check/2");
50
is($files_a_123_infos->[1]->{file_type}, 'text/plain', "GetFilesInfo() result check/3");
51
is($files_a_123_infos->[1]->{file_size}, 19, "GetFilesInfo() result check/4");
52
is($files_a_123_infos->[1]->{file_description}, 'File #2 sample description', "GetFilesInfo() result check/5");
53
54
## GetFile() result checks
55
is($mf_a_123->GetFile(), undef, "GetFile() result check/1");
56
is($mf_a_123->GetFile(id => 0), undef, "GetFile() result check/2");
57
58
my $a123_file_1 = $mf_a_123->GetFile(id => $files_a_123_infos->[0]->{file_id});
59
is(ref($a123_file_1), 'HASH', "GetFile() result check/3");
60
is($a123_file_1->{file_id}, $files_a_123_infos->[0]->{file_id}, "GetFile() result check/4");
61
is($a123_file_1->{file_content}, "file contents\n1111\n", "GetFile() result check/5");
62
63
## MergeFileRecIds() tests
64
$mf_a_123->MergeFileRecIds(123,221);
65
$files_a_123_infos = $mf_a_123->GetFilesInfo();
66
is(scalar @$files_a_123_infos, 2, "GetFilesInfo() result count after dummy MergeFileRecIds()");
67
$mf_a_123->MergeFileRecIds(124);
68
$files_a_123_infos = $mf_a_123->GetFilesInfo();
69
is(scalar @$files_a_123_infos, 4, "GetFilesInfo() result count after MergeFileRecIds()/1");
70
is(scalar @{$mf_a_124->GetFilesInfo()}, 0, "GetFilesInfo() result count after MergeFileRecIds()/2");
71
is($files_a_123_infos->[-1]->{file_name}, 'File_name_4.txt', "GetFilesInfo() result check after MergeFileRecIds()");
72
73
## DelFile() test
74
$mf_a_123->DelFile(id => $files_a_123_infos->[-1]->{file_id});
75
$files_a_123_infos = $mf_a_123->GetFilesInfo();
76
is(scalar @$files_a_123_infos, 3, "GetFilesInfo() result count after DelFile()");
77
78
## DelAllFiles() tests
79
$mf_a_123->DelAllFiles();
80
$files_a_123_infos = $mf_a_123->GetFilesInfo();
81
is(scalar @$files_a_123_infos, 0, "GetFilesInfo() result count after DelAllFiles()/1");
82
$mf_b_221->DelAllFiles();
83
is(scalar @{$mf_b_221->GetFilesInfo()}, 0, "GetFilesInfo() result count after DelAllFiles()/2");
84
85
$dbh->rollback;
86
87
1;

Return to bug 3050