($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 = ?" :
plural => 'tickets',
pk => 'id',
},
+ ticket_updates => {
+ module => 'Koha::Ticket::Updates',
+ singular => 'ticket_update',
+ plural => 'ticket_updates',
+ pk => 'id',
+ },
issues => {
module => 'Koha::Checkouts',
singular => 'checkout',
};
}
+=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;
=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
-
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
- "null"
format: date-time
description: Date the ticket was resolved_date
+ updates_count:
+ type:
+ - integer
+ - "null"
+ description: Number of updates
additionalProperties: false
required:
- title
--- /dev/null
+---
+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
- reporter
- resolver
- biblio
+ - updates+count
collectionFormat: csv
responses:
"200":
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
$ref: ./definitions/suggestion.yaml
ticket:
$ref: ./definitions/ticket.yaml
+ ticket_update:
+ $ref: ./definitions/ticket_update.yaml
transfer_limit:
$ref: ./definitions/transfer_limit.yaml
vendor:
$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:
--- /dev/null
+#!/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;
}
);
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'";
}
}
- ""
- "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: ""
white-space: pre-wrap;
}
+.wrapfix {
+ white-space: pre-wrap;
+}
+
pre {
background-color: transparent;
border: 0;
</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 ) %]
<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 %]
</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 ) %]
<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 %]
--- /dev/null
+[% USE raw %]
+[% USE Asset %]
+[% SET footerjs = 1 %]
+[% USE TablesSettings %]
+[% INCLUDE 'doc-head-open.inc' %]
+<title>
+ Catalog concerns › Tools › 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">×</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' %]
<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">
</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>:
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
use Koha::Suggestions;
use Koha::BackgroundJobs;
use Koha::CurbsidePickups;
+use Koha::Tickets;
my $query = CGI->new;
)->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;
--- /dev/null
+#!/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;
+};