Browse Source

Bug 31028: Add catalog concern management page to staff

This patch adds a catalog concern management page to the staff client
accessible via the cataloging home page and a new 'Pending catalog
concerns' link on the front page.

This includes added the requisit ticket_updates api endpoints and notice
triggers and templates for notifying patrons of changes to their
reported concerns.

Test plan
1) Enable the `OpacCatalogConcerns` system preference
2) Catalog concern management is tied to your users ability to edit the
   catalog, `editcatalogue`.
3) Confirm that you can see 'Catalog concerns' listed on the cataloging
   home page if you have the `editcatalogue` permission and not if you
   do not.
4) Add a new concern as an opac user.
5) Confirm that once a concern is present in the system you see a count
   of 'catalog concerns pending' on the intranet main page if you have
   the `editcatalogue` permission.
6) Click through either the cataloging home page or pending concerns
   link on the main page to view the new concerns management page.
7) Confirm the table displays as one would expect.
8) Confirm clicking on details or the concern title exposes a 'details'
   modal with the option to add an update or resolve the concern.
9) Verify that if selecting 'notify' when updateing or resolving a
   concern triggers a notice to be sent to the opac user who first
   reported the issue.

Signed-off-by: David Nind <david@davidnind.com>
Signed-off-by: Helen Oliver <HOliver@tavi-port.ac.uk>

Signed-off-by: Kyle M Hall <kyle@bywatersolutions.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
23.05.x
Martin Renvoize 11 months ago
committed by Tomas Cohen Arazi
parent
commit
dcf7688b94
  1. 7
      C4/Letters.pm
  2. 104
      Koha/REST/V1/Tickets.pm
  3. 11
      Koha/Ticket/Update.pm
  4. 17
      admin/columns_settings.yml
  5. 5
      api/v1/swagger/definitions/ticket.yaml
  6. 37
      api/v1/swagger/definitions/ticket_update.yaml
  7. 103
      api/v1/swagger/paths/tickets.yaml
  8. 4
      api/v1/swagger/swagger.yaml
  9. 37
      cataloguing/concerns.pl
  10. 16
      installer/data/mysql/atomicupdate/bug_31028.pl
  11. 36
      installer/data/mysql/en/mandatory/sample_notices.yml
  12. 4
      koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss
  13. 7
      koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc
  14. 8
      koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt
  15. 306
      koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt
  16. 9
      koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt
  17. 16
      koha-tmpl/intranet-tmpl/prog/js/datatables.js
  18. 12
      mainpage.pl
  19. 202
      t/db_dependent/api/v1/ticket_updates.t

7
C4/Letters.pm

@ -739,6 +739,7 @@ sub _parseletter_sth {
($table eq 'biblio' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
($table eq 'biblioitems' ) ? "SELECT * FROM $table WHERE biblionumber = ?" :
($table eq 'tickets' ) ? "SELECT * FROM $table WHERE id = ?" :
($table eq 'ticket_updates' ) ? "SELECT * FROM $table WHERE id = ?" :
($table eq 'credits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
($table eq 'debits' ) ? "SELECT * FROM accountlines WHERE accountlines_id = ?" :
($table eq 'items' ) ? "SELECT * FROM $table WHERE itemnumber = ?" :
@ -1745,6 +1746,12 @@ sub _get_tt_params {
plural => 'tickets',
pk => 'id',
},
ticket_updates => {
module => 'Koha::Ticket::Updates',
singular => 'ticket_update',
plural => 'ticket_updates',
pk => 'id',
},
issues => {
module => 'Koha::Checkouts',
singular => 'checkout',

104
Koha/REST/V1/Tickets.pm

@ -154,4 +154,108 @@ sub delete {
};
}
=head3 list_updates
=cut
sub list_updates {
my $c = shift->openapi->valid_input or return;
return try {
my $ticket = Koha::Tickets->find( $c->validation->param('ticket_id') );
unless ($ticket) {
return $c->render(
status => 404,
openapi => { error => "Ticket not found" }
);
}
my $updates_set = $ticket->updates;
my $updates = $c->objects->search($updates_set);
return $c->render( status => 200, openapi => $updates );
}
catch {
$c->unhandled_exception($_);
};
}
=head3 add_update
=cut
sub add_update {
my $c = shift->openapi->valid_input or return;
my $patron = $c->stash('koha.user');
my $ticket_id_param = $c->validation->param('ticket_id');
my $ticket_update = $c->validation->param('body');
$ticket_update->{ticket_id} //= $ticket_id_param;
if ( $ticket_update->{ticket_id} != $ticket_id_param ) {
return $c->render(
status => 400,
openapi => { error => "Ticket Mismatch" }
);
}
# Set reporter from session
$ticket_update->{user_id} = $patron->id;
# FIXME: We should allow impersonation at a later date to
# allow an API user to submit on behalf of a user
return try {
my $state = delete $ticket_update->{state};
# Store update
my $update = Koha::Ticket::Update->new_from_api($ticket_update)->store;
$update->discard_changes;
# Update ticket state if needed
if ( defined($state) && $state eq 'resolved' ) {
my $ticket = $update->ticket;
$ticket->set(
{
resolver_id => $update->user_id,
resolved_date => $update->date
}
)->store;
}
# Optionally add to message_queue here to notify reporter
if ( $update->public ) {
my $notice =
( defined($state) && $state eq 'resolved' )
? 'TICKET_RESOLVE'
: 'TICKET_UPDATE';
my $letter = C4::Letters::GetPreparedLetter(
module => 'catalog',
letter_code => $notice,
branchcode => $update->user->branchcode,
tables => { ticket_updates => $update->id }
);
if ($letter) {
my $message_id = C4::Letters::EnqueueLetter(
{
letter => $letter,
borrowernumber => $update->ticket->reporter_id,
message_transport_type => 'email',
}
);
}
}
# Return
$c->res->headers->location(
$c->req->url->to_string . '/' . $update->id );
return $c->render(
status => 201,
openapi => $update->to_api
);
}
catch {
$c->unhandled_exception($_);
};
}
1;

11
Koha/Ticket/Update.pm

@ -57,6 +57,17 @@ sub user {
=head2 Internal methods
=head3 to_api_mapping
This method returns the mapping for representing a Koha::Ticket::Update object
on the API.
=cut
sub to_api_mapping {
return { id => 'update_id', };
}
=head3 _type
=cut

17
admin/columns_settings.yml

@ -643,6 +643,23 @@ modules:
-
columnname: stocknumber
concerns:
table_concerns:
default_sort_order: 0
columns:
-
columnname: reported
-
columnname: details
-
columnname: title
-
columnname: status
-
columnname: actions
cannot_be_toggled: 1
cannot_be_modified: 1
z3950_search:
resultst:
default_sort_order: 1

5
api/v1/swagger/definitions/ticket.yaml

@ -53,6 +53,11 @@ properties:
- "null"
format: date-time
description: Date the ticket was resolved_date
updates_count:
type:
- integer
- "null"
description: Number of updates
additionalProperties: false
required:
- title

37
api/v1/swagger/definitions/ticket_update.yaml

@ -0,0 +1,37 @@
---
type: object
properties:
update_id:
type: integer
description: Internal ticket update identifier
readOnly: true
ticket_id:
type: integer
description: Internal ticket identifier
readOnly: true
user:
type:
- object
- "null"
description: The object representing the patron who added the update
readOnly: true
user_id:
type: integer
description: Internal identifier for the patron who added the update
date:
type:
- string
- "null"
format: date-time
description: Date the ticket update was reported
readOnly: true
message:
type: string
description: Ticket update details
public:
type: boolean
description: Is this update intended to be sent to the patron
additionalProperties: true
required:
- message
- public

103
api/v1/swagger/paths/tickets.yaml

@ -28,6 +28,7 @@
- reporter
- resolver
- biblio
- updates+count
collectionFormat: csv
responses:
"200":
@ -210,6 +211,108 @@
x-koha-authorization:
permissions:
editcatalogue: edit_catalogue
"/tickets/{ticket_id}/updates":
get:
x-mojo-to: Tickets#list_updates
operationId: listTicketUpdates
tags:
- tickets
summary: List ticket updates
produces:
- application/json
parameters:
- $ref: "../swagger.yaml#/parameters/ticket_id_pp"
- $ref: "../swagger.yaml#/parameters/match"
- $ref: "../swagger.yaml#/parameters/order_by"
- $ref: "../swagger.yaml#/parameters/page"
- $ref: "../swagger.yaml#/parameters/per_page"
- $ref: "../swagger.yaml#/parameters/q_param"
- $ref: "../swagger.yaml#/parameters/q_body"
- $ref: "../swagger.yaml#/parameters/q_header"
- $ref: "../swagger.yaml#/parameters/request_id_header"
- name: x-koha-embed
in: header
required: false
description: Embed list sent as a request header
type: array
items:
type: string
enum:
- user
collectionFormat: csv
responses:
"200":
description: A list of ticket updates
schema:
type: array
items:
$ref: "../swagger.yaml#/definitions/ticket_update"
"403":
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
"404":
description: Ticket not found
schema:
$ref: "../swagger.yaml#/definitions/error"
"500":
description: |
Internal server error. Possible `error_code` attribute values:
* `internal_server_error`
schema:
$ref: "../swagger.yaml#/definitions/error"
"503":
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
catalogue: "1"
post:
x-mojo-to: Tickets#add_update
operationId: addTicketUpdate
tags:
- tickets
summary: Add an update to the ticket
parameters:
- $ref: "../swagger.yaml#/parameters/ticket_id_pp"
- name: body
in: body
description: A ticket update object
required: true
schema:
$ref: "../swagger.yaml#/definitions/ticket_update"
produces:
- application/json
responses:
"201":
description: Ticket added
schema:
$ref: "../swagger.yaml#/definitions/ticket_update"
"401":
description: Authentication required
schema:
$ref: "../swagger.yaml#/definitions/error"
"403":
description: Access forbidden
schema:
$ref: "../swagger.yaml#/definitions/error"
"404":
description: Ticket not found
schema:
$ref: "../swagger.yaml#/definitions/error"
"500":
description: Internal error
schema:
$ref: "../swagger.yaml#/definitions/error"
"503":
description: Under maintenance
schema:
$ref: "../swagger.yaml#/definitions/error"
x-koha-authorization:
permissions:
editcatalogue: edit_catalogue
/public/tickets:
post:
x-mojo-to: Tickets#add

4
api/v1/swagger/swagger.yaml

@ -94,6 +94,8 @@ definitions:
$ref: ./definitions/suggestion.yaml
ticket:
$ref: ./definitions/ticket.yaml
ticket_update:
$ref: ./definitions/ticket_update.yaml
transfer_limit:
$ref: ./definitions/transfer_limit.yaml
vendor:
@ -329,6 +331,8 @@ paths:
$ref: "./paths/tickets.yaml#/~1tickets"
"/tickets/{ticket_id}":
$ref: "./paths/tickets.yaml#/~1tickets~1{ticket_id}"
"/tickets/{ticket_id}/updates":
$ref: "./paths/tickets.yaml#/~1tickets~1{ticket_id}~1updates"
/transfer_limits:
$ref: ./paths/transfer_limits.yaml#/~1transfer_limits
/transfer_limits/batch:

37
cataloguing/concerns.pl

@ -0,0 +1,37 @@
#!/usr/bin/perl
# This file is part of Koha.
#
# 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 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 WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use CGI qw ( -utf8 );
use C4::Context;
use C4::Auth qw( get_template_and_user );
use C4::Output qw( output_html_with_http_headers );
my $query = CGI->new;
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
{
template_name => "cataloguing/concerns.tt",
query => $query,
type => "intranet",
flagsrequired => { cataloguing => '*' },
}
);
output_html_with_http_headers $query, $cookie, $template->output;
1;

16
installer/data/mysql/atomicupdate/bug_31028.pl

@ -93,5 +93,21 @@ return {
}
);
say $out "Added new notice 'TICKET_ACKNOWLEDGEMENT'";
$dbh->do(
q{
INSERT IGNORE INTO letter(module,code,branchcode,name,is_html,title,content,message_transport_type)
VALUES ( 'catalog', 'TICKET_UPDATE', '', 'Concern updated', '1', 'Catalog concern updated', "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %],\r\n\r\nThe library has added an update to the concern you reported against [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %].\r\n\r\nThe following comment was left: \r\n[% ticket_update.message %]\r\n\r\nThankyou", 'email' );
}
);
say $out "Added new notice 'TICKET_UPDATE'";
$dbh->do(
q{
INSERT IGNORE INTO letter(module,code,branchcode,name,is_html,title,content,message_transport_type)
VALUES ( 'catalog', 'TICKET_RESOLVE', '', 'Concern resolved', '1', 'Catalog concern resolved', "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %],\r\n\r\nThe library has now marked your concern with [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %] as resolved.\r\n\r\nThe following comment was left: \r\n[% ticket_update.message %]\r\n\r\nThankyou", 'email' );
}
);
say $out "Added new notice 'TICKET_RESOLVE'";
}
}

36
installer/data/mysql/en/mandatory/sample_notices.yml

@ -61,6 +61,42 @@ tables:
- ""
- "Thankyou"
- module: catalog
code: TICKET_RESOLVE
branchcode: ""
name: "Concern resolved"
is_html: 1
title: "Catalog concern resolved"
message_transport_type: email
lang: default
content:
- "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %],"
- ""
- "The library has now marked your concern with [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %] as resolved."
- ""
- "The following comment was left: "
- "[% ticket_update.message %]"
- ""
- "Thankyou"
- module: catalog
code: TICKET_UPDATE
branchcode: ""
name: "Concern updated"
is_html: 1
title: "Catalog concern updated"
message_transport_type: email
lang: default
content:
- "Dear [% INCLUDE 'patron-title.inc' patron => ticket_update.ticket.reporter %],"
- ""
- "The library has added an update to the concern you reported against [% INCLUDE 'biblio-title.inc' biblio=ticket_update.ticket.biblio link = 0 %]."
- ""
- "The following comment was left: "
- "[% ticket_update.message %]"
- ""
- "Thankyou"
- module: circulation
code: ACCOUNT_CREDIT
branchcode: ""

4
koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss

@ -3301,6 +3301,10 @@ label {
white-space: pre-wrap;
}
.wrapfix {
white-space: pre-wrap;
}
pre {
background-color: transparent;
border: 0;

7
koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc

@ -29,7 +29,7 @@
</ul>
[% END %]
[% IF ( CAN_user_tools_inventory ) %]
[% IF ( CAN_user_tools_inventory || ( Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue ) ) %]
<h5>Reports</h5>
<ul>
[% IF ( CAN_user_tools_inventory ) %]
@ -37,6 +37,11 @@
<a href="/cgi-bin/koha/tools/inventory.pl">Inventory</a>
</li>
[% END %]
[% IF Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue %]
<li>
<a href="/cgi-bin/koha/cataloguing/concerns.pl">Catalog concerns</a>
</li>
[% END %]
</ul>
[% END %]

8
koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt

@ -91,7 +91,7 @@
</ul>
[% END %]
[% IF ( CAN_user_tools_inventory ) %]
[% IF ( CAN_user_tools_inventory || ( Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue ) ) %]
<h3>Reports</h3>
<ul class="buttons-list">
[% IF ( CAN_user_tools_inventory ) %]
@ -99,6 +99,12 @@
<a class="circ-button" href="/cgi-bin/koha/tools/inventory.pl"><i class="fa fa-line-chart"></i> Inventory</a>
</li>
[% END %]
[% IF ( Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue ) %]
<li>
<a class="circ-button" href="/cgi-bin/koha/cataloguing/concerns.pl"><i class="fa fa-list-ul"></i> Catalog concerns</a>
</li>
[% END %]
</ul>
[% END %]

306
koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt

@ -0,0 +1,306 @@
[% USE raw %]
[% USE Asset %]
[% SET footerjs = 1 %]
[% USE TablesSettings %]
[% INCLUDE 'doc-head-open.inc' %]
<title>
Catalog concerns &rsaquo; Tools &rsaquo; Koha
</title>
[% INCLUDE 'doc-head-close.inc' %]
</head>
<body id="cat_concerns" class="cat">
[% INCLUDE 'header.inc' %]
[% INCLUDE 'cataloging-search.inc' %]
<nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumb">
<ol>
<li>
<a href="/cgi-bin/koha/mainpage.pl">Home</a>
</li>
<li>
<a href="/cgi-bin/koha/cataloguing/cataloging-home.pl">Cataloging</a>
</li>
<li>
<a href="#" aria-current="page">
Catalog concerns
</a>
</li>
</ol>
</nav>
<div class="main container-fluid">
<div class="row">
<div class="col-sm-10 col-sm-push-2">
<main>
<h1>Concerns</h1>
<div class="page-section">
<fieldset class="action" style="cursor:pointer;">
<a id="hideResolved"><i class="fa fa-minus-square"></i> Hide resolved</a>
| <a id="showAll"><i class="fa fa-bars"></i> Show all</a>
</fieldset>
<table id="table_concerns">
<thead>
<tr>
<th>Reported</th>
<th>Details</th>
<th>Title</th>
<th>Status</th>
<th data-class-name="actions noExport">Actions</th>
</tr>
</thead>
</table>
</div>
</main>
</div> <!-- /.col-sm-10.col-sm-push-2 -->
<div class="col-sm-2 col-sm-pull-10">
<aside>
[% INCLUDE 'cat-menu.inc' %]
</aside>
</div> <!-- /.col-sm-2.col-sm-pull-10 -->
</div> <!-- /.row -->
<!-- Display updates concern modal -->
<div class="modal" id="ticketDetailsModal" tabindex="-1" role="dialog" aria-labelledby="ticketDetailsLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="closebtn" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="displayUpdateLabel">Ticket details</h4>
</div>
<div class="modal-body">
<div id="concern-details"></div>
<fieldset class="rows">
<ol>
<li>
<label for="message">Update: </label>
<textarea id="update_message" name="message"></textarea>
</li>
<li>
<label for="public">Notify: </label>
<input type="checkbox" name="public" id="public">
</li>
</ol>
</fieldset>
</div> <!-- /.modal-body -->
<div class="modal-footer">
<input type="hidden" name="ticket_id" id="ticket_id">
<button type="button" class="btn btn-default" id="resolveTicket">Resolve</button>
<button type="submit" class="btn btn-primary" id="updateTicket">Comment</button>
</div> <!-- /.modal-footer -->
</div> <!-- /.modal-content -->
</div> <!-- /.modal-dialog -->
</div> <!-- /#displayUpdateModal -->
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'datatables.inc' %]
[% INCLUDE 'columns_settings.inc' %]
[% INCLUDE 'js-date-format.inc' %]
[% INCLUDE 'js-patron-format.inc' %]
[% INCLUDE 'js-biblio-format.inc' %]
<script>
$(document).ready(function() {
var logged_in_user_borrowernumber = "[% logged_in_user.borrowernumber | html %]";
var table_settings = [% TablesSettings.GetTableSettings('cataloguing', 'concerns', 'table_concerns', 'json') | $raw %];
var tickets_url = '/api/v1/tickets';
var tickets = $("#table_concerns").kohaTable({
"ajax": {
"url": tickets_url
},
"embed": [
"reporter",
"resolver",
"biblio",
"updates+count",
],
'emptyTable': '<div class="dialog message">' + _("Congratulations, there are no catalog concerns.") + '</div>',
"columnDefs": [{
"targets": [0, 1, 2, 3],
"render": function(data, type, row, meta) {
if (type == 'display') {
if (data != null) {
return data.escapeHtml();
} else {
return "";
}
}
return data;
}
}],
"columns": [{
"data": "reported_date:reporter.firstname",
"render": function(data, type, row, meta) {
let reported = '<span class="date clearfix">' + $datetime(row.reported_date) + '</span>';
reported += '<span class="reporter clearfix">' + $patron_to_html(row.reporter, {
display_cardnumber: false,
url: true
}) + '</span>';
return reported;
},
"searchable": true,
"orderable": true
},
{
"data": "title:body",
"render": function(data, type, row, meta) {
let result = '<a role="button" href="#" data-toggle="modal" data-target="#ticketDetailsModal" data-concern="' + encodeURIComponent(row.ticket_id) + '">' + row.title + '</a>';
if (row.updates_count) {
result += '<span class="pull-right"><a role="button" href="#" data-toggle="modal" data-target="#ticketDetailsModal" data-concern="' + encodeURIComponent(row.ticket_id) + '"><i class="fa fa-comment" aria-hidden="true"></i> ' + row.updates_count + '</a></span>';
}
result += '<div id="detail_' + row.ticket_id + '" class="hidden">' + row.body + '</div>';
return result;
},
"searchable": true,
"orderable": true
},
{
"data": "biblio.title",
"render": function(data, type, row, meta) {
return $biblio_to_html(row.biblio, {
link: 1
});
},
"searchable": true,
"orderable": true
},
{
"data": "resolver.firstname:resolver.surname:resolved_date",
"render": function(data, type, row, meta) {
let result = '';
if (row.resolved_date) {
result += _("Resolved by:") + ' <span>' + $patron_to_html(row.resolver, {
display_cardnumber: false,
url: true
}) + '</span>';
result += '<span class="clearfix">' + $datetime(row.resolved_date) + '</span>';
} else {
result += _("Open");
}
return result;
},
"searchable": true,
"orderable": true
},
{
"data": function(row, type, val, meta) {
let result = '<a class="btn btn-default btn-xs" role="button" href="#" data-toggle="modal" data-target="#ticketDetailsModal" data-concern="' + encodeURIComponent(row.ticket_id) + '"><i class="fa fa-eye" aria-hidden="true"></i> ' + _("Details") + '</a>';
return result;
},
"searchable": false,
"orderable": false
},
]
}, table_settings, 1);
$('#hideResolved').on("click", function() {
// It would be great if we could pass null here but it gets stringified
tickets.DataTable().columns('3').search('special:undefined').draw();
});
$('#showAll').on("click", function() {
tickets.DataTable().columns('3').search('').draw();
});
$('#ticketDetailsModal').on('show.bs.modal', function(event) {
let modal = $(this);
let button = $(event.relatedTarget);
let ticket_id = button.data('concern');
modal.find('.modal-footer input').val(ticket_id);
let detail = $('#detail_' + ticket_id).text();
let display = '<div class="list-group">';
display += '<div class="list-group-item">';
display += '<span class="wrapfix">' + detail + '</span>';
display += '</div>';
display += '<div id="concern-updates" class="list-group-item">';
display += '<span>Loading updates . . .</span>';
display += '</div>';
display += '</div>';
let details = modal.find('#concern-details');
details.html(display);
$.ajax({
url: "/api/v1/tickets/" + ticket_id + "/updates",
method: "GET",
headers: {
"x-koha-embed": "user"
},
}).success(function(data) {
let updates_display = $('#concern-updates');
let updates = '';
data.forEach(function(item, index) {
updates += '<div class="list-group-item">';
updates += '<span class="wrapfix">' + item.message + '</span>';
updates += '<span class="clearfix">' + $patron_to_html(item.user, {
display_cardnumber: false,
url: true
}) + ' (' + $datetime(item.date) + ')</span>';
updates += '</div>';
});
updates_display.html(updates);
}).error(function() {
});
});
$('#ticketDetailsModal').on('click', '#updateTicket', function(e) {
let ticket_id = $('#ticket_id').val();
let params = {
'public': $('#public').is(":checked"),
message: $('#update_message').val(),
user_id: logged_in_user_borrowernumber
};
$.ajax({
url: "/api/v1/tickets/" + ticket_id + "/updates",
method: "POST",
data: JSON.stringify(params),
ontentType: "application/json; charset=utf-8"
}).success(function() {
$("#ticketDetailsModal").modal('hide');
tickets.DataTable().ajax.reload(function(data) {
$("#concern_action_result_dialog").hide();
$("#concern_delete_success").html(_("Concern #%s updated successfully.").format(ticket_id)).show();
});
}).error(function() {
$("#concern_update_error").html(_("Error resolving concern #%s. Check the logs.").format(ticket_id)).show();
});
});
$('#ticketDetailsModal').on('click', '#resolveTicket', function(e) {
let ticket_id = $('#ticket_id').val();
let params = {
'public': $('#public').is(":checked"),
message: $('#update_message').val(),
user_id: logged_in_user_borrowernumber,
state: 'resolved'
};
$.ajax({
url: "/api/v1/tickets/" + ticket_id + "/updates",
method: "POST",
data: JSON.stringify(params),
ontentType: "application/json; charset=utf-8"
}).success(function() {
$("#ticketDetailsModal").modal('hide');
tickets.DataTable().ajax.reload(function(data) {
$("#concern_action_result_dialog").hide();
$("#concern_delete_success").html(_("Concern #%s updated successfully.").format(ticket_id)).show();
});
}).error(function() {
$("#concern_update_error").html(_("Error resolving concern #%s. Check the logs.").format(ticket_id)).show();
});
});
});
</script>
[% END %]
[% INCLUDE 'intranet-bottom.inc' %]

9
koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt

@ -172,7 +172,7 @@
<div class="row">
<div class="col-sm-12">
[%# Following statement must be in one line for translatability %]
[% IF ( CAN_user_tools_moderate_comments && pendingcomments ) || ( CAN_user_tools_moderate_tags && pendingtags ) || ( CAN_user_borrowers_edit_borrowers && pending_borrower_modifications ) || ( CAN_user_suggestions_suggestions_manage && ( pendingsuggestions || all_pendingsuggestions )) || ( CAN_user_borrowers_edit_borrowers && pending_discharge_requests ) || pending_article_requests || ( Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count ) || ( Koha.Preference('OPACReportProblem') && CAN_user_problem_reports && pending_problem_reports.count ) || already_ran_jobs || new_curbside_pickups.count %]
[% IF ( CAN_user_tools_moderate_comments && pendingcomments ) || ( CAN_user_tools_moderate_tags && pendingtags ) || ( CAN_user_borrowers_edit_borrowers && pending_borrower_modifications ) || ( CAN_user_suggestions_suggestions_manage && ( pendingsuggestions || all_pendingsuggestions )) || ( CAN_user_borrowers_edit_borrowers && pending_discharge_requests ) || pending_article_requests || ( Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count ) || ( Koha.preference('OpacCatalogConcerns') && pending_biblio_tickets && CAN_user_editcatalogue_edit_catalogue ) || ( Koha.Preference('OPACReportProblem') && CAN_user_problem_reports && pending_problem_reports.count ) || already_ran_jobs || new_curbside_pickups.count %]
<div id="area-pending" class="page-section">
[% IF pending_article_requests %]
<div class="pending-info" id="article_requests_pending">
@ -227,6 +227,13 @@
</div>
[% END %]
[% IF ( Koha.Preference('OpacCatalogConcerns') && CAN_user_editcatalogue_edit_catalogue ) %]
<div class="pending-info" id="catalog_concerns_pending">
<a href="/cgi-bin/koha/cataloguing/concerns.pl">Catalog concerns pending</a>:
<span class="pending-number-link">[% pending_biblio_tickets | html %]</span>
</div>
[% END %]
[% IF Koha.Preference('AllowCheckoutNotes') && CAN_user_circulate_manage_checkout_notes && pending_checkout_notes.count %]
<div class="pending-info" id="checkout_notes_pending">
<a href="/cgi-bin/koha/circ/checkout-notes.pl">Checkout notes pending</a>:

16
koha-tmpl/intranet-tmpl/prog/js/datatables.js

@ -586,12 +586,18 @@ jQuery.fn.dataTable.ext.errMode = function(settings, note, message) {
var part = {};
var attr = attributes[i];
let criteria = options.criteria;
if ( value.match(/^\^(.*)\$$/) ) {
value = value.replace(/^\^/, '').replace(/\$$/, '');
if ( value === 'special:undefined' ) {
value = null;
criteria = "exact";
} else {
// escape SQL LIKE special characters % and _
value = value.replace(/(\%|\\)/g, "\\$1");
}
if ( value !== null ) {
if ( value.match(/^\^(.*)\$$/) ) {
value = value.replace(/^\^/, '').replace(/\$$/, '');
criteria = "exact";
} else {
// escape SQL LIKE special characters %
value = value.replace(/(\%|\\)/g, "\\$1");
}
}
part[!attr.includes('.')?'me.'+attr:attr] = criteria === 'exact'
? value

12
mainpage.pl

@ -35,6 +35,7 @@ use Koha::Quotes;
use Koha::Suggestions;
use Koha::BackgroundJobs;
use Koha::CurbsidePickups;
use Koha::Tickets;
my $query = CGI->new;
@ -103,6 +104,17 @@ my $pending_article_requests = Koha::ArticleRequests->search_limited(
)->count;
my $pending_problem_reports = Koha::ProblemReports->search({ status => 'New' });
if ( C4::Context->preference('OpacCatalogConcerns') ) {
my $pending_biblio_tickets = Koha::Tickets->search(
{
resolved_date => undef,
biblio_id => { '!=' => undef }
}
);
$template->param(
pending_biblio_tickets => $pending_biblio_tickets->count );
}
unless ( $logged_in_user->has_permission( { parameters => 'manage_background_jobs' } ) ) {
my $already_ran_jobs = Koha::BackgroundJobs->search(
{ borrowernumber => $logged_in_user->borrowernumber } )->count ? 1 : 0;

202
t/db_dependent/api/v1/ticket_updates.t

@ -0,0 +1,202 @@
#!/usr/bin/env perl
# This file is part of Koha.
#
# 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 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 WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Koha; if not, see <http://www.gnu.org/licenses>.
use Modern::Perl;
use Test::More tests => 2;
use Test::Mojo;
use t::lib::TestBuilder;
use t::lib::Mocks;
use Koha::Tickets;
use Koha::Database;
my $schema = Koha::Database->new->schema;
my $builder = t::lib::TestBuilder->new;
my $t = Test::Mojo->new('Koha::REST::V1');
t::lib::Mocks::mock_preference( 'RESTBasicAuth', 1 );
subtest 'list_updates() tests' => sub {
plan tests => 14;
$schema->storage->txn_begin;
Koha::Tickets->search->delete;
my $librarian = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 2**2 } # catalogue flag = 2
}
);
my $password = 'thePassword123';
$librarian->set_password( { password => $password, skip_validation => 1 } );
my $userid = $librarian->userid;
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 0 }
}
);
$patron->set_password( { password => $password, skip_validation => 1 } );
my $unauth_userid = $patron->userid;
my $ticket = $builder->build_object( { class => 'Koha::Tickets' } );
my $ticket_id = $ticket->id;
## Authorized user tests
# No updates, so empty array should be returned
$t->get_ok("//$userid:$password@/api/v1/tickets/$ticket_id/updates")
->status_is(200)->json_is( [] );
my $update = $builder->build_object(
{
class => 'Koha::Ticket::Updates',
value => { ticket_id => $ticket_id }
}
);
# One ticket update added, should get returned
$t->get_ok("//$userid:$password@/api/v1/tickets/$ticket_id/updates")
->status_is(200)->json_is( [ $update->to_api ] );
my $update_2 = $builder->build_object(
{
class => 'Koha::Ticket::Updates',
value => { ticket_id => $ticket_id }
}
);
my $update_3 = $builder->build_object(
{
class => 'Koha::Ticket::Updates',
value => { ticket_id => $ticket_id }
}
);
# Two ticket updates added, they should both be returned
$t->get_ok("//$userid:$password@/api/v1/tickets/$ticket_id/updates")
->status_is(200)
->json_is( [ $update->to_api, $update_2->to_api, $update_3->to_api, ] );
# Warn on unsupported query parameter
$t->get_ok(
"//$userid:$password@/api/v1/tickets/$ticket_id/updates?ticket_blah=blah"
)->status_is(400)->json_is(
[
{
path => '/query/ticket_blah',
message => 'Malformed query string'
}
]
);
# Unauthorized access
$t->get_ok("//$unauth_userid:$password@/api/v1/tickets")->status_is(403);
$schema->storage->txn_rollback;
};
subtest 'add_update() tests' => sub {
plan tests => 17;
$schema->storage->txn_begin;
my $librarian = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 2**9 } # editcatalogue flag = 9
}
);
my $password = 'thePassword123';
$librarian->set_password( { password => $password, skip_validation => 1 } );
my $userid = $librarian->userid;
my $patron = $builder->build_object(
{
class => 'Koha::Patrons',
value => { flags => 0 }
}
);
$patron->set_password( { password => $password, skip_validation => 1 } );
my $unauth_userid = $patron->userid;
my $ticket = $builder->build_object( { class => 'Koha::Tickets' } );
my $ticket_id = $ticket->id;
my $update = {
message => "First ticket update",
public => Mojo::JSON->false
};
# Unauthorized attempt to write
$t->post_ok(
"//$unauth_userid:$password@/api/v1/tickets/$ticket_id/updates" =>
json => $update )->status_is(403);
# Authorized attempt to write
my $update_id =
$t->post_ok(
"//$userid:$password@/api/v1/tickets/$ticket_id/updates" => json =>
$update )->status_is( 201, 'SWAGGER3.2.1' )->header_like(
Location => qr|^\/api\/v1\/tickets/\d*|,
'SWAGGER3.4.1'
)->json_is( '/message' => $update->{message} )
->json_is( '/public' => $update->{public} )
->json_is( '/user_id' => $librarian->id )->tx->res->json->{update_id};
# Authorized attempt to create with null id
$update->{update_id} = undef;
$t->post_ok(
"//$userid:$password@/api/v1/tickets/$ticket_id/updates" => json =>
$update )->status_is(400)->json_has('/errors');
# Authorized attempt to create with existing id
$update->{update_id} = $update_id;
$t->post_ok(
"//$userid:$password@/api/v1/tickets/$ticket_id/updates" => json =>
$update )->status_is(400)->json_is(
"/errors" => [
{
message => "Read-only.",
path => "/body/update_id"
}
]
);
# Authorized attempt to write missing data
my $update_with_missing_field = { message => "Another ticket update" };
$t->post_ok(
"//$userid:$password@/api/v1/tickets/$ticket_id/updates" => json =>
$update_with_missing_field )->status_is(400)->json_is(
"/errors" => [
{
message => "Missing property.",
path => "/body/public"
}
]
);
$schema->storage->txn_rollback;
};
Loading…
Cancel
Save