Bugzilla – Attachment 146246 Details for
Bug 31028
Add 'Report a concern' feature for patrons to report concerns about catalog records
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 31028: Add catalog concern management page to staff
Bug-31028-Add-catalog-concern-management-page-to-s.patch (text/plain), 49.57 KB, created by
Martin Renvoize (ashimema)
on 2023-02-06 11:13:42 UTC
(
hide
)
Description:
Bug 31028: Add catalog concern management page to staff
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2023-02-06 11:13:42 UTC
Size:
49.57 KB
patch
obsolete
>From 90099956fa22102c5a77c4ca6b18d6afc604c9e0 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@ptfs-europe.com> >Date: Tue, 25 Oct 2022 14:03:11 +0100 >Subject: [PATCH] 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> >--- > C4/Letters.pm | 7 + > Koha/REST/V1/Tickets.pm | 106 +++++- > Koha/Ticket/Update.pm | 11 + > admin/columns_settings.yml | 17 + > api/v1/swagger/definitions/ticket.yaml | 5 + > api/v1/swagger/definitions/ticket_update.yaml | 37 +++ > api/v1/swagger/paths/tickets.yaml | 103 ++++++ > api/v1/swagger/swagger.yaml | 4 + > cataloguing/concerns.pl | 37 +++ > .../data/mysql/atomicupdate/bug_31028.pl | 16 + > .../mysql/en/mandatory/sample_notices.yml | 36 +++ > .../prog/css/src/staff-global.scss | 4 + > .../prog/en/includes/cat-menu.inc | 7 +- > .../en/modules/cataloguing/cataloging-home.tt | 8 +- > .../prog/en/modules/cataloguing/concerns.tt | 306 ++++++++++++++++++ > .../prog/en/modules/intranet-main.tt | 9 +- > koha-tmpl/intranet-tmpl/prog/js/datatables.js | 16 +- > mainpage.pl | 12 + > t/db_dependent/api/v1/ticket_updates.t | 202 ++++++++++++ > 19 files changed, 934 insertions(+), 9 deletions(-) > create mode 100644 api/v1/swagger/definitions/ticket_update.yaml > create mode 100755 cataloguing/concerns.pl > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt > create mode 100644 t/db_dependent/api/v1/ticket_updates.t > >diff --git a/C4/Letters.pm b/C4/Letters.pm >index ab99a6c511f..4233890dfae 100644 >--- a/C4/Letters.pm >+++ b/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 = ?" : >@@ -1741,6 +1742,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', >diff --git a/Koha/REST/V1/Tickets.pm b/Koha/REST/V1/Tickets.pm >index 5fd1c0f4321..1422eb2d323 100644 >--- a/Koha/REST/V1/Tickets.pm >+++ b/Koha/REST/V1/Tickets.pm >@@ -81,7 +81,7 @@ sub add { > > return try { > my $body = $c->validation->param('body'); >- >+ > # Set reporter from session > $body->{reporter_id} = $patron->id; > # FIXME: We should allow impersonation at a later date to >@@ -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; >diff --git a/Koha/Ticket/Update.pm b/Koha/Ticket/Update.pm >index b954d59f03d..f275a32df6f 100644 >--- a/Koha/Ticket/Update.pm >+++ b/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 >diff --git a/admin/columns_settings.yml b/admin/columns_settings.yml >index d4da6b08abf..c4129adef26 100644 >--- a/admin/columns_settings.yml >+++ b/admin/columns_settings.yml >@@ -635,6 +635,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 >diff --git a/api/v1/swagger/definitions/ticket.yaml b/api/v1/swagger/definitions/ticket.yaml >index 06a64e867ae..57e4cfd54da 100644 >--- a/api/v1/swagger/definitions/ticket.yaml >+++ b/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 >diff --git a/api/v1/swagger/definitions/ticket_update.yaml b/api/v1/swagger/definitions/ticket_update.yaml >new file mode 100644 >index 00000000000..8806ebe1f6e >--- /dev/null >+++ b/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 >diff --git a/api/v1/swagger/paths/tickets.yaml b/api/v1/swagger/paths/tickets.yaml >index 26bf2afa446..709d5509b5e 100644 >--- a/api/v1/swagger/paths/tickets.yaml >+++ b/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 >diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml >index 9c763bd21d0..5b7828fd86b 100644 >--- a/api/v1/swagger/swagger.yaml >+++ b/api/v1/swagger/swagger.yaml >@@ -90,6 +90,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: >@@ -319,6 +321,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: >diff --git a/cataloguing/concerns.pl b/cataloguing/concerns.pl >new file mode 100755 >index 00000000000..4e3d1ff55e8 >--- /dev/null >+++ b/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; >diff --git a/installer/data/mysql/atomicupdate/bug_31028.pl b/installer/data/mysql/atomicupdate/bug_31028.pl >index 683c92003ce..68015ef4fbe 100644 >--- a/installer/data/mysql/atomicupdate/bug_31028.pl >+++ b/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'"; > } > } >diff --git a/installer/data/mysql/en/mandatory/sample_notices.yml b/installer/data/mysql/en/mandatory/sample_notices.yml >index c6d1edf2271..7648361715b 100644 >--- a/installer/data/mysql/en/mandatory/sample_notices.yml >+++ b/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: "" >diff --git a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >index 31404446409..57826a39151 100644 >--- a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >+++ b/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss >@@ -3290,6 +3290,10 @@ label { > white-space: pre-wrap; > } > >+.wrapfix { >+ white-space: pre-wrap; >+} >+ > pre { > background-color: transparent; > border: 0; >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc >index 2bd37f8fc97..024b0c47e9e 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-menu.inc >+++ b/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 %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt >index 70c1a822144..dddb7264568 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/cataloging-home.tt >+++ b/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 %] > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/concerns.tt >new file mode 100644 >index 00000000000..7a87b95cc33 >--- /dev/null >+++ b/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 › 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' %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt >index 75f5cfc6dea..c50a99e93a0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/intranet-main.tt >@@ -177,7 +177,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"> >@@ -232,6 +232,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>: >diff --git a/koha-tmpl/intranet-tmpl/prog/js/datatables.js b/koha-tmpl/intranet-tmpl/prog/js/datatables.js >index 2e922c64a63..05d110f1e96 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/datatables.js >+++ b/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 >diff --git a/mainpage.pl b/mainpage.pl >index f5872e65bbb..a1bc19d4d5f 100755 >--- a/mainpage.pl >+++ b/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; >diff --git a/t/db_dependent/api/v1/ticket_updates.t b/t/db_dependent/api/v1/ticket_updates.t >new file mode 100644 >index 00000000000..f4df715f533 >--- /dev/null >+++ b/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; >+}; >-- >2.39.1
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 31028
:
137412
|
139222
|
139223
|
139224
|
139225
|
139226
|
139227
|
139228
|
139229
|
139230
|
139231
|
139232
|
139233
|
139234
|
139235
|
139236
|
139237
|
139238
|
139572
|
139573
|
139574
|
139575
|
139576
|
139577
|
139578
|
139579
|
139580
|
139581
|
139582
|
139583
|
139584
|
139585
|
139586
|
139587
|
139588
|
139589
|
139590
|
139591
|
140289
|
140290
|
140291
|
140292
|
140293
|
140294
|
140295
|
140296
|
140297
|
140298
|
140299
|
140300
|
140301
|
140302
|
140303
|
140304
|
140305
|
140306
|
140307
|
140308
|
140436
|
140437
|
140438
|
140439
|
140440
|
140441
|
140442
|
140443
|
140444
|
140445
|
140446
|
140447
|
140448
|
140449
|
140450
|
140451
|
140452
|
140453
|
140454
|
140455
|
140764
|
140765
|
140766
|
140767
|
140768
|
140769
|
140770
|
140771
|
140772
|
140773
|
140774
|
140775
|
140776
|
140777
|
140778
|
140779
|
140780
|
140781
|
140782
|
140783
|
140793
|
140929
|
140930
|
140931
|
140932
|
140933
|
140934
|
140935
|
140936
|
140937
|
140938
|
140939
|
140940
|
140941
|
140942
|
140943
|
140944
|
140945
|
140946
|
140947
|
140948
|
140949
|
140950
|
141133
|
141134
|
141135
|
141136
|
141137
|
141138
|
141139
|
141140
|
141141
|
141142
|
141143
|
141144
|
141145
|
141146
|
141147
|
141148
|
141149
|
141150
|
141151
|
141152
|
141153
|
141154
|
141440
|
141441
|
141442
|
141443
|
141444
|
141445
|
141446
|
141447
|
141448
|
141449
|
141450
|
141451
|
141452
|
141453
|
141454
|
141455
|
141456
|
141457
|
141458
|
141459
|
141460
|
142164
|
142165
|
142166
|
142167
|
142168
|
142169
|
142170
|
142171
|
142172
|
142173
|
142174
|
142175
|
142177
|
142179
|
142180
|
142181
|
142182
|
142183
|
142184
|
142185
|
142186
|
142187
|
142188
|
142189
|
142190
|
142191
|
142616
|
142617
|
142618
|
142619
|
142620
|
142621
|
142622
|
142623
|
142624
|
142625
|
142626
|
142627
|
142932
|
142933
|
142934
|
142935
|
142936
|
142937
|
142938
|
142939
|
142940
|
142941
|
142942
|
142943
|
142944
|
142945
|
142946
|
142947
|
142948
|
142949
|
143238
|
143239
|
143240
|
143241
|
143242
|
143243
|
143244
|
143245
|
143246
|
143247
|
143248
|
143249
|
143250
|
143251
|
143252
|
143253
|
143254
|
143255
|
143256
|
143257
|
143302
|
143303
|
143304
|
143305
|
143306
|
143307
|
143308
|
143309
|
143310
|
143311
|
143312
|
143313
|
143314
|
143315
|
143316
|
143317
|
143318
|
143319
|
143320
|
143321
|
143322
|
143323
|
143324
|
143325
|
143326
|
143327
|
143328
|
143329
|
143330
|
143331
|
143332
|
143333
|
143334
|
143335
|
143336
|
143337
|
143338
|
143339
|
143340
|
143341
|
143470
|
143471
|
143472
|
143473
|
143474
|
143475
|
143476
|
143477
|
143478
|
143479
|
143480
|
143481
|
143482
|
143483
|
143484
|
143485
|
143486
|
143487
|
143488
|
143489
|
143490
|
143647
|
143648
|
143649
|
143650
|
143651
|
143652
|
143653
|
143654
|
143655
|
143656
|
143657
|
143658
|
143659
|
143660
|
143661
|
143662
|
143663
|
143664
|
143665
|
143666
|
143667
|
143708
|
143709
|
143710
|
143711
|
143712
|
143713
|
143714
|
143715
|
143716
|
143717
|
143718
|
143719
|
143720
|
143721
|
143722
|
143723
|
143724
|
143725
|
143726
|
143727
|
144647
|
144648
|
144649
|
144650
|
144651
|
144652
|
144653
|
144654
|
144655
|
144656
|
144657
|
144658
|
144659
|
144660
|
144661
|
144662
|
144663
|
144664
|
144665
|
144666
|
144667
|
144668
|
145542
|
146214
|
146215
|
146216
|
146217
|
146218
|
146219
|
146220
|
146221
|
146222
|
146223
|
146224
|
146225
|
146226
|
146227
|
146228
|
146229
|
146230
|
146231
|
146232
|
146233
|
146234
|
146235
|
146236
|
146237
|
146238
|
146239
|
146240
|
146241
|
146242
|
146243
|
146244
|
146245
|
146246
|
146247
|
146248
|
146249
|
146250
|
146251
|
146252
|
146253
|
146254
|
146255
|
146256
|
146257
|
146258
|
146259
|
146260
|
146261
|
146262
|
146697
|
146870
|
146871
|
146872
|
146873
|
146874
|
146875
|
146876
|
146877
|
146878
|
146879
|
146880
|
146881
|
146882
|
146883
|
146884
|
146885
|
146886
|
146887
|
146888
|
146889
|
146890
|
146891
|
146892
|
146893
|
146894
|
146895
|
146896
|
147797
|
147840
|
147841
|
147844
|
147859
|
147860
|
147861
|
147866
|
148673
|
149931