From 9c27c13aa22b52ed1b37e376913a4c19bfb3abd0 Mon Sep 17 00:00:00 2001 From: Martin Stenberg Date: Sun, 23 Aug 2015 18:22:50 +0200 Subject: [PATCH] Bug 14224: Allow patron notes about item shown at check in This patch adds a "Note" input field to checked out items in the "your summery" section. The field allows patrons to write notes about the item checked out, such as "this DVD is scratched", "the binding was torn", etc. The note will be emailed to the library and displayed on item check in. Patch adds two fields to the "issues" table - "note" and "notedate". Patch adds syspref "AllowIssueNotes" - default off. Test Plan: 1) Apply this patch 2) Run updatedatabase 3) Make sure the branch has an email address specified 4) Check out an item to a patron 5) Log in with the patron you checked out the item for 6) In "your summery", write a message in the new "Note" field for the item checked out. Hit ENTER to save/send. 7) Success message should be seen and an email sent to the email address specified for the branch which the item was checked out from. 8) Log in with staff account and check in the item 9) The note should be shown The note can be changed by the patron at any time while the item is checked out. Each change will send a new email to the branch. Only the latest note version will be shown on check in. =============================================================================== NOTE: - This patch is not yet ready for testing. This patch fixes merge conflicts and fixes the atomic update so this the database updates correctly. - Still to do: - GetIssue/GetIssues - subroutine for ajax code - moving ajax part to svc/ Sponsored-by: Catalyst IT --- C4/Circulation.pm | 41 ++++++- circ/returns.pl | 3 + .../bug_14224-add_new_issue_columns.sql | 2 + .../atomicupdate/bug_14224-issue_notes_syspref.sql | 1 + installer/data/mysql/kohastructure.sql | 30 +++++ installer/data/mysql/sysprefs.sql | 1 + .../en/modules/admin/preferences/circulation.pref | 6 + .../intranet-tmpl/prog/en/modules/circ/returns.tt | 10 ++ .../bootstrap/en/modules/opac-sendissuenote.tt | 21 ++++ .../opac-tmpl/bootstrap/en/modules/opac-user.tt | 96 +++++++++++++++ opac/opac-user.pl | 133 ++++++++++++++++++++- 11 files changed, 340 insertions(+), 4 deletions(-) create mode 100644 installer/data/mysql/atomicupdate/bug_14224-add_new_issue_columns.sql create mode 100644 installer/data/mysql/atomicupdate/bug_14224-issue_notes_syspref.sql create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-sendissuenote.tt diff --git a/C4/Circulation.pm b/C4/Circulation.pm index 0cac5ed..46fee88 100644 --- a/C4/Circulation.pm +++ b/C4/Circulation.pm @@ -96,6 +96,7 @@ BEGIN { &GetBranchItemRule &GetBiblioIssues &GetOpenIssue + &SetIssueNote &AnonymiseIssueHistory &CheckIfIssuedToPatron &IsItemIssued @@ -2540,7 +2541,7 @@ sub GetOpenIssue { =head2 GetIssues $issues = GetIssues({}); # return all issues! - $issues = GetIssues({ borrowernumber => $borrowernumber, biblionumber => $biblionumber }); + $issues = GetIssues({ borrowernumber => $borrowernumber, biblionumber => $biblionumber, issue_id => $issue_id }); Returns all pending issues that match given criteria. Returns a arrayref or undef if an error occurs. @@ -2555,6 +2556,8 @@ Allowed criteria are: =item * itemnumber +=item * issue_id + =back =cut @@ -2564,7 +2567,7 @@ sub GetIssues { # Build filters my @filters; - my @allowed = qw(borrowernumber biblionumber itemnumber); + my @allowed = qw(borrowernumber biblionumber itemnumber issue_id); foreach (@allowed) { if (defined $criteria->{$_}) { push @filters, { @@ -4133,6 +4136,40 @@ sub _CalculateAndUpdateFine { } } +=head2 SetIssueNote + + &SetIssueNote($issue_id, $note); + +Sets a note to the issuenotes table for the given issue. + +=over 4 + +=item C<$issue_id> is the id of the issue for which to set the note + +=item C<$note> is the note to set + +=back + +Returns: + True on success + False on failure + +=cut + +sub SetIssueNote { + my ( $issue_id, $note) = @_; + + my $dbh = C4::Context->dbh; + + unless ( $issue_id =~ /\d+/ ) { + return; + } + + my $query = "UPDATE issues SET notedate=NOW(),note=? WHERE issue_id=?"; + my $sth = $dbh->prepare($query); + return $sth->execute( $note, $issue_id ); +} + 1; __END__ diff --git a/circ/returns.pl b/circ/returns.pl index ec13c04..a521438 100755 --- a/circ/returns.pl +++ b/circ/returns.pl @@ -285,6 +285,8 @@ if ($barcode) { $materials = $av->count ? $av->next->lib : ''; } + my $issue = GetItemIssue($itemnumber); + $template->param( title => $biblio->{'title'}, homebranch => $biblio->{'homebranch'}, @@ -298,6 +300,7 @@ if ($barcode) { biblionumber => $biblio->{'biblionumber'}, borrower => $borrower, additional_materials => $materials, + issue => $issue, ); my %input = ( diff --git a/installer/data/mysql/atomicupdate/bug_14224-add_new_issue_columns.sql b/installer/data/mysql/atomicupdate/bug_14224-add_new_issue_columns.sql new file mode 100644 index 0000000..97f21e1 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_14224-add_new_issue_columns.sql @@ -0,0 +1,2 @@ +ALTER IGNORE TABLE issues ADD `note` mediumtext default NULL; +ALTER IGNORE TABLE issues ADD `notedate` datetime default NULL; diff --git a/installer/data/mysql/atomicupdate/bug_14224-issue_notes_syspref.sql b/installer/data/mysql/atomicupdate/bug_14224-issue_notes_syspref.sql new file mode 100644 index 0000000..e619b21 --- /dev/null +++ b/installer/data/mysql/atomicupdate/bug_14224-issue_notes_syspref.sql @@ -0,0 +1 @@ +INSERT IGNORE INTO systempreferences (`variable`, `value`, `options`, `explanation`,`type`) VALUES ('AllowIssueNotes', '0', NULL, 'Allow patrons to submit notes about checked out items.','YesNo'); diff --git a/installer/data/mysql/kohastructure.sql b/installer/data/mysql/kohastructure.sql index f00b63b..95c49ff 100644 --- a/installer/data/mysql/kohastructure.sql +++ b/installer/data/mysql/kohastructure.sql @@ -843,6 +843,36 @@ CREATE TABLE `import_items` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; -- +-- Table structure for table `issues` +-- + +DROP TABLE IF EXISTS `issues`; +CREATE TABLE `issues` ( -- information related to check outs or issues + `issue_id` int(11) NOT NULL AUTO_INCREMENT, -- primary key for issues table + `borrowernumber` int(11), -- foreign key, linking this to the borrowers table for the patron this item was checked out to + `itemnumber` int(11), -- foreign key, linking this to the items table for the item that was checked out + `date_due` datetime default NULL, -- datetime the item is due (yyyy-mm-dd hh:mm::ss) + `branchcode` varchar(10) default NULL, -- foreign key, linking to the branches table for the location the item was checked out + `returndate` datetime default NULL, -- date the item was returned, will be NULL until moved to old_issues + `lastreneweddate` datetime default NULL, -- date the item was last renewed + `return` varchar(4) default NULL, + `renewals` tinyint(4) default NULL, -- lists the number of times the item was renewed + `auto_renew` BOOLEAN default FALSE, -- automatic renewal + `timestamp` timestamp NOT NULL default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP, -- the date and time this record was last touched + `issuedate` datetime default NULL, -- date the item was checked out or issued + `onsite_checkout` int(1) NOT NULL default 0, -- in house use flag + `note` mediumtext default NULL, -- issue note text + `notedate` datetime default NULL, -- datetime of issue note (yyyy-mm-dd hh:mm::ss) + PRIMARY KEY (`issue_id`), + KEY `issuesborridx` (`borrowernumber`), + KEY `itemnumber_idx` (`itemnumber`), + KEY `branchcode_idx` (`branchcode`), + KEY `bordate` (`borrowernumber`,`timestamp`), + CONSTRAINT `issues_ibfk_1` FOREIGN KEY (`borrowernumber`) REFERENCES `borrowers` (`borrowernumber`) ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT `issues_ibfk_2` FOREIGN KEY (`itemnumber`) REFERENCES `items` (`itemnumber`) ON DELETE RESTRICT ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci; + +-- -- Table structure for table `issuingrules` -- diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 5a90b7c..8b09312 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -27,6 +27,7 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, ` ('AllowNotForLoanOverride','0','','If ON, Koha will allow the librarian to loan a not for loan item.','YesNo'), ('AllowOfflineCirculation','0','','If on, enables HTML5 offline circulation functionality.','YesNo'), ('AllowPatronToSetCheckoutsVisibilityForGuarantor', '0', NULL, 'If enabled, the patron can set checkouts to be visible to his or her guarantor', 'YesNo'), +('AllowIssueNotes', '0', NULL, 'Allow patrons to submit notes about checked out items.','YesNo'), ('AllowPKIAuth','None','None|Common Name|emailAddress','Use the field from a client-side SSL certificate to look a user in the Koha database','Choice'), ('AllowPurchaseSuggestionBranchChoice','0','1','Allow user to choose branch when making a purchase suggestion','YesNo'), ('AllowRenewalIfOtherItemsAvailable','0',NULL,'If enabled, allow a patron to renew an item with unfilled holds if other available items can fill that hold.','YesNo'), diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref index fc9706b..a07c30e 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/circulation.pref @@ -136,6 +136,12 @@ Circulation: yes: Show no: "Do not show" - all items in the "Checked-in items" list, even items that were not checked out. + - + - pref: AllowIssueNotes + choices: + yes: Allow + no: "Don't allow" + - patrons to submit notes about checked out items. Checkout Policy: - diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt index c0b9c90..a6ad652 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/returns.tt @@ -183,6 +183,16 @@ $(document).ready(function () { [% END %] + +[% IF ( issue.note) %] +
+

Patron note

+

[% issue.notedate %]

+

[% title |html %] [% author %]

+

[% issue.note %]

+
+[% END %] + [% IF ( fines ) %]
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-sendissuenote.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-sendissuenote.tt new file mode 100644 index 0000000..e4a655b --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-sendissuenote.tt @@ -0,0 +1,21 @@ + +Issue note for [% title %] [% author %] + + +[% USE HtmlToText %] + +
+[% FILTER html2text %] +

Hi,

+

[% MEMBER.firstname %] [% MEMBER.surname %] sent you the following note related to the check out of +

[% title %] [% author %].

+[% END %] + + + + +[% FILTER html2text %] + [% note %] +[% END %] + + diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt index b7ac6f9..c0c91aa 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt @@ -120,6 +120,8 @@ Using this account is not recommended because some parts of Koha will not functi
[% END # / IF patron_flagged %] + + [% SET OPACMySummaryNote = Koha.Preference('OPACMySummaryNote') %] [% IF OPACMySummaryNote %][% OPACMySummaryNote %][% END %] @@ -164,6 +166,9 @@ Using this account is not recommended because some parts of Koha will not functi [% IF ( OPACMySummaryHTML ) %] Links [% END %] + [% IF ( AllowIssueNotes ) %] + Note + [% END %] @@ -280,6 +285,20 @@ Using this account is not recommended because some parts of Koha will not functi [% IF ( OPACMySummaryHTML ) %] [% ISSUE.MySummaryHTML %] [% END %] + [% IF ( AllowIssueNotes ) %] + + + + + + [% END %] [% END # /FOREACH ISSUES %] @@ -782,6 +801,83 @@ Using this account is not recommended because some parts of Koha will not functi [% END %] [% END %] + [% IF ( AllowIssueNotes ) %] + $("input[name='note']").keyup(function(e){ + /* prevent submitting of renewselected form */ + if(e.which == 13) + e.preventDefault(); + + var $btn_save = $('#save_'+$(this).data('issue_id')); + var origvalue = $(this).data('origvalue'); + var value = $(this).val(); + + if(origvalue != value) { + if(origvalue != "") + $btn_save.text('Submit changes'); + else + $btn_save.text('Submit note'); + $btn_save.show(); + } else { + $btn_save.hide(); + } + }); + + $("a[name='submitnote']").click(function(e){ + var $self = $(this); + var title = $(this).parent().siblings('.title').html(); + var $noteinput = $(this).siblings('input[name="note"]').first(); + + var ajaxData = { + 'action': 'issuenote', + 'issue_id': $noteinput.data('issue_id'), + 'note': $noteinput.val(), + }; + + $.ajax({ + url: '/cgi-bin/koha/opac-user.pl', + type: 'POST', + dataType: 'json', + data: ajaxData, + }) + .done(function(data) { + var message = ""; + if(data.status == 'saved') { + $("#notesaved").removeClass("alert-error"); + $("#notesaved").addClass("alert-info"); + $noteinput.data('origvalue', data.note); + $noteinput.val(data.note); + message = "

Your note about " + title + " was saved and have been sent to the library.

"; + $self.hide(); + } else if(data.status == 'removed') { + $("#notesaved").removeClass("alert-error"); + $("#notesaved").addClass("alert-info"); + $noteinput.data('origvalue', ""); + $noteinput.val(""); + message = "

Your note about " + title + " was removed.

"; + $self.hide(); + } else { + $("#notesaved").removeClass("alert-info"); + $("#notesaved").addClass("alert-error"); + message = "

Your note about " + title + " could not be saved.

" + + "

" + data.error + "

"; + } + + message += "

" + data.note + "

"; + $("#notesaved").html(message); + }) + .fail(function(data) { + $("#notesaved").removeClass("alert-info"); + $("#notesaved").addClass("alert-error"); + var message = "

Your note about " + title + " could not be saved.

" + + "

Ajax request has failed.

"; + $("#notesaved").html(message); + }) + .always(function() { + $("#notesaved").show(); + }); + }); + [% END %] + $( ".suspend-until" ).datepicker({ minDate: 1 }); // Require that "until date" be in the future }); //]]> diff --git a/opac/opac-user.pl b/opac/opac-user.pl index 0ac78de..dfa1404 100755 --- a/opac/opac-user.pl +++ b/opac/opac-user.pl @@ -22,17 +22,24 @@ use strict; use CGI qw ( -utf8 ); -use C4::Auth; +use Mail::Sendmail; +use MIME::QuotedPrint; +use Carp; +use C4::Auth qw(:DEFAULT check_cookie_auth); use C4::Koha; use C4::Circulation; use C4::Reserves; use C4::Members; use C4::Members::AttributeTypes; use C4::Members::Attributes qw/GetBorrowerAttributeValue/; -use C4::Output; +use CGI::Cookie; # need to check cookies before having CGI parse the POST request +use C4::Output qw(:DEFAULT :ajax); +use C4::Scrubber; use C4::Biblio; use C4::Items; use C4::Letters; +use C4::Branch; # GetBranches +use Koha::Email; use Koha::DateUtils; use Koha::Holds; use Koha::Database; @@ -57,6 +64,126 @@ BEGIN { } } +sub ajax_auth_cgi { # returns CGI object + my $needed_flags = shift; + my %cookies = fetch CGI::Cookie; + my $input = CGI->new; + my $sessid = $cookies{'CGISESSID'}->value; + my ($auth_status, $auth_sessid) = check_cookie_auth($sessid, $needed_flags); + if ($auth_status ne "ok") { + output_with_http_headers $input, undef, + "window.alert('Your CGI session cookie ($sessid) is not current. " . + "Please refresh the page and try again.');\n", 'js'; + exit 0; + } + return $input; +} + +# AJAX requests +my $is_ajax = is_ajax(); +my $query = ($is_ajax) ? &ajax_auth_cgi({}) : CGI->new(); +if ($is_ajax) { + my $action = $query->param('action'); + + # Issue Note + if ( $action == 'issuenote' && C4::Context->preference('AllowIssueNotes') ) { + my $scrubber = C4::Scrubber->new(); + my $note = $query->param('note'); + my $issue_id = $query->param('issue_id'); + my $clean_note = $scrubber->scrub($note); + my $status = "saved"; + my $error = ""; + my ($error, $member, $issue); + + my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { + template_name => "opac-sendissuenote.tt", + query => $query, + type => "opac", + authnotrequired => 1, + } + ); + + # verify issue_id + if ( $issue_id =~ /\d+/ ) { + $member = GetMember(borrowernumber => $borrowernumber); + ($issue) = @{C4::Circulation::GetIssues({issue_id => $issue_id})}; + + if ( $issue->{'borrowernumber'} != $borrowernumber ) { + $status = "fail"; + $error = "Invalid issue id!"; + } + } else { + $status = "fail"; + $error = "Invalid issue id!"; + } + + if ( (not $error) && SetIssueNote($issue_id, $clean_note) ) { + if($clean_note) { # only send email if note not empty + my $branch = GetBranchDetail($issue->{'branchcode'}); + + if ( $branch->{'branchemail'} ) { + my $biblio = GetBiblioFromItemNumber($issue->{'itemnumber'}); + my $message = Koha::Email->new(); + my %mail = $message->create_message_headers(); + + $template->param( + author => $biblio->{'author'}, + title => $biblio->{'title'}, + MEMBER => $member, + note => $clean_note, + ); + + # Getting template result + my $template_res = $template->output(); + my $body; + + # Analysing information and getting mail properties + if ( $template_res =~ /(.*)/s ) { + $mail{subject} = $1; + $mail{subject} =~ s|\n?(.*)\n?|$1|; + } + else { $mail{'subject'} = "no subject"; } + $mail{subject} = Encode::encode("UTF-8", $mail{subject}); + + my $email_header = ""; + if ( $template_res =~ /
(.*)/s ) { + $email_header = $1; + $email_header =~ s|\n?(.*)\n?|$1|; + $email_header = encode_qp(Encode::encode("UTF-8", $email_header)); + } + + if ( $template_res =~ /(.*)/s ) { + $body = $1; + $body =~ s|\n?(.*)\n?|$1|; + $body = encode_qp(Encode::encode("UTF-8", $body)); + } + + $mail{to} = $branch->{'branchemail'}; + $mail{from} = $member->{'email'} || ($branch->{'branchemail'}=~s/^.+@/noreply@/ && $branch->{'branchemail'}); + $mail{body} = $email_header . $body; + + unless ( sendmail(%mail) ) { + $status = "fail"; + $error = "Could not send message to library. Message will still show at check in."; + carp "Error sending mail: $Mail::Sendmail::error \n"; + } + } + } else { # note empty, i.e removed + $status = "removed"; + } + } else { + $status = "fail"; + $error = "Perhaps the item has already been check in?"; + } + + my $response = "{\"status\": \"$status\", \"note\": \"$clean_note\", \"issue_id\": \"$issue_id\", \"error\": \"$error\"}"; + output_with_http_headers($query, undef, $response, 'js'); + exit; + } # END Issue Note +} + + my ( $template, $borrowernumber, $cookie ) = get_template_and_user( { template_name => "opac-user.tt", @@ -258,6 +385,8 @@ if ($issues){ } my $overduesblockrenewing = C4::Context->preference('OverduesBlockRenewing'); $canrenew = 0 if ($overduesblockrenewing ne 'allow' and $overdues_count == $count); + +$template->param( AllowIssueNotes => C4::Context->preference('AllowIssueNotes') ); $template->param( ISSUES => \@issuedat ); $template->param( issues_count => $count ); $template->param( canrenew => $canrenew ); -- 2.1.4