Bugzilla – Attachment 54951 Details for
Bug 13895
Add routes for checkouts retrieval and renewal
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 13895: Add API routes for checkouts retrieval and renewal
Bug-13895-Add-API-routes-for-checkouts-retrieval-a.patch (text/plain), 19.87 KB, created by
Benjamin Rokseth
on 2016-08-28 16:39:21 UTC
(
hide
)
Description:
Bug 13895: Add API routes for checkouts retrieval and renewal
Filename:
MIME Type:
Creator:
Benjamin Rokseth
Created:
2016-08-28 16:39:21 UTC
Size:
19.87 KB
patch
obsolete
>From fbced2cc677a225fb14aaa1e0d78c03bde9efae9 Mon Sep 17 00:00:00 2001 >From: Julian Maurice <julian.maurice@biblibre.com> >Date: Mon, 23 Mar 2015 13:10:46 +0100 >Subject: [PATCH] Bug 13895: Add API routes for checkouts retrieval and renewal > >GET /checkouts?borrowernumber={borrowernumber} >GET /checkouts/{checkout_id} >PUT /checkouts/{checkout_id} > >+ unit tests in t/db_dependent/api/v1/checkouts.t > >Test plan: >1. Open a browser tab on Koha staff and log in (to create CGISESSID > cookie). You should have permission circulate_remaining_permissions. >2. Go to http://yourlibrary/api/v1/checkouts?borrowernumber=XXX (replace > XXX with a borrowernumber that has checkouts) and check you receive > correct data >3. Go to http://yourlibrary/api/v1/checkouts/YYY (replace YYY with an > existing checkout id) and check you receive correct data >4. Send PUT requests to http://yourlibrary/api/v1/checkouts/YYY until > the maximum number of renewals is reached (you should have a 403 > error) >5. Run unit tests in t/db_dependent/api/v1/checkouts.t > >Depends on bugs 16699 and 14868 > >Signed-off-by: Benjamin Rokseth <benjamin.rokseth@kul.oslo.kommune.no> > >Signed-off-by: Lari Taskula <larit@student.uef.fi> >--- > Koha/REST/V1/Checkout.pm | 89 ++++++++++++ > api/v1/swagger/definitions.json | 6 + > api/v1/swagger/definitions/checkout.json | 48 +++++++ > api/v1/swagger/definitions/checkouts.json | 6 + > api/v1/swagger/parameters.json | 3 + > api/v1/swagger/parameters/checkout.json | 9 ++ > api/v1/swagger/paths.json | 6 + > api/v1/swagger/paths/checkouts.json | 99 +++++++++++++ > t/db_dependent/api/v1/checkouts.t | 228 ++++++++++++++++++++++++++++++ > 9 files changed, 494 insertions(+) > create mode 100644 Koha/REST/V1/Checkout.pm > create mode 100644 api/v1/swagger/definitions/checkout.json > create mode 100644 api/v1/swagger/definitions/checkouts.json > create mode 100644 api/v1/swagger/parameters/checkout.json > create mode 100644 api/v1/swagger/paths/checkouts.json > create mode 100644 t/db_dependent/api/v1/checkouts.t > >diff --git a/Koha/REST/V1/Checkout.pm b/Koha/REST/V1/Checkout.pm >new file mode 100644 >index 0000000..ac5dea5 >--- /dev/null >+++ b/Koha/REST/V1/Checkout.pm >@@ -0,0 +1,89 @@ >+package Koha::REST::V1::Checkout; >+ >+# 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, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Mojo::Base 'Mojolicious::Controller'; >+ >+use C4::Auth qw( haspermission ); >+use C4::Context; >+use C4::Circulation; >+use Koha::Issues; >+ >+sub list { >+ my ($c, $args, $cb) = @_; >+ >+ my $borrowernumber = $c->param('borrowernumber'); >+ my $checkouts = C4::Circulation::GetIssues({ >+ borrowernumber => $borrowernumber >+ }); >+ >+ $c->$cb($checkouts, 200); >+} >+ >+sub get { >+ my ($c, $args, $cb) = @_; >+ >+ my $checkout_id = $args->{checkout_id}; >+ my $checkout = Koha::Issues->find($checkout_id); >+ >+ if (!$checkout) { >+ return $c->$cb({ >+ error => "Checkout doesn't exist" >+ }, 404); >+ } >+ >+ return $c->$cb($checkout->unblessed, 200); >+} >+ >+sub renew { >+ my ($c, $args, $cb) = @_; >+ >+ my $checkout_id = $args->{checkout_id}; >+ my $checkout = Koha::Issues->find($checkout_id); >+ >+ if (!$checkout) { >+ return $c->$cb({ >+ error => "Checkout doesn't exist" >+ }, 404); >+ } >+ >+ my $borrowernumber = $checkout->borrowernumber; >+ my $itemnumber = $checkout->itemnumber; >+ >+ # Disallow renewal if OpacRenewalAllowed is off and user has insufficient rights >+ unless (C4::Context->preference('OpacRenewalAllowed')) { >+ my $user = $c->stash('koha.user'); >+ unless ($user && haspermission($user->userid, { circulate => "circulate_remaining_permissions" })) { >+ return $c->$cb({error => "Opac Renewal not allowed"}, 403); >+ } >+ } >+ >+ my ($can_renew, $error) = C4::Circulation::CanBookBeRenewed( >+ $borrowernumber, $itemnumber); >+ >+ if (!$can_renew) { >+ return $c->$cb({error => "Renewal not authorized ($error)"}, 403); >+ } >+ >+ AddRenewal($borrowernumber, $itemnumber, $checkout->branchcode); >+ $checkout = Koha::Issues->find($checkout_id); >+ >+ return $c->$cb($checkout->unblessed, 200); >+} >+ >+1; >diff --git a/api/v1/swagger/definitions.json b/api/v1/swagger/definitions.json >index e4d7427..4136043 100644 >--- a/api/v1/swagger/definitions.json >+++ b/api/v1/swagger/definitions.json >@@ -2,6 +2,12 @@ > "patron": { > "$ref": "definitions/patron.json" > }, >+ "checkouts": { >+ "$ref": "definitions/checkouts.json" >+ }, >+ "checkout": { >+ "$ref": "definitions/checkout.json" >+ }, > "holds": { > "$ref": "definitions/holds.json" > }, >diff --git a/api/v1/swagger/definitions/checkout.json b/api/v1/swagger/definitions/checkout.json >new file mode 100644 >index 0000000..6eadd55 >--- /dev/null >+++ b/api/v1/swagger/definitions/checkout.json >@@ -0,0 +1,48 @@ >+{ >+ "type": "object", >+ "properties": { >+ "issue_id": { >+ "type": "string", >+ "description": "internally assigned checkout identifier" >+ }, >+ "borrowernumber": { >+ "$ref": "../x-primitives.json#/borrowernumber" >+ }, >+ "itemnumber": { >+ "$ref": "../x-primitives.json#/itemnumber" >+ }, >+ "date_due": { >+ "description": "Due date" >+ }, >+ "branchcode": { >+ "$ref": "../x-primitives.json#/branchcode" >+ }, >+ "issuingbranch": { >+ "description": "Code of the branch where issue was made" >+ }, >+ "returndate": { >+ "description": "Date the item was returned" >+ }, >+ "lastreneweddate": { >+ "description": "Date the item was last renewed" >+ }, >+ "return": { >+ "description": "?" >+ }, >+ "renewals": { >+ "description": "Number of renewals" >+ }, >+ "auto_renew": { >+ "description": "Auto renewal" >+ }, >+ "timestamp": { >+ "description": "Last update time" >+ }, >+ "issuedate": { >+ "description": "Date the item was issued" >+ }, >+ "onsite_checkout": { >+ "description": "On site checkout" >+ } >+ } >+} >diff --git a/api/v1/swagger/definitions/checkouts.json b/api/v1/swagger/definitions/checkouts.json >new file mode 100644 >index 0000000..b6562f9 >--- /dev/null >+++ b/api/v1/swagger/definitions/checkouts.json >@@ -0,0 +1,6 @@ >+{ >+ "type": "array", >+ "items": { >+ "$ref": "checkout.json" >+ } >+} >diff --git a/api/v1/swagger/parameters.json b/api/v1/swagger/parameters.json >index b20e19f..9586fd7 100644 >--- a/api/v1/swagger/parameters.json >+++ b/api/v1/swagger/parameters.json >@@ -7,5 +7,8 @@ > }, > "holdIdPathParam": { > "$ref": "parameters/hold.json#/holdIdPathParam" >+ }, >+ "checkoutIdPathParam": { >+ "$ref": "parameters/checkout.json#/checkoutIdPathParam" > } > } >diff --git a/api/v1/swagger/parameters/checkout.json b/api/v1/swagger/parameters/checkout.json >new file mode 100644 >index 0000000..8dfa3d2 >--- /dev/null >+++ b/api/v1/swagger/parameters/checkout.json >@@ -0,0 +1,9 @@ >+{ >+ "checkoutIdPathParam": { >+ "name": "checkout_id", >+ "in": "path", >+ "description": "Internal checkout identifier", >+ "required": true, >+ "type": "integer" >+ } >+} >diff --git a/api/v1/swagger/paths.json b/api/v1/swagger/paths.json >index f00b1b4..05c29aa 100644 >--- a/api/v1/swagger/paths.json >+++ b/api/v1/swagger/paths.json >@@ -5,6 +5,12 @@ > "/holds/{reserve_id}": { > "$ref": "paths/holds.json#/~1holds~1{reserve_id}" > }, >+ "/checkouts": { >+ "$ref": "paths/checkouts.json#/~1checkouts" >+ }, >+ "/checkouts/{checkout_id}": { >+ "$ref": "paths/checkouts.json#/~1checkouts~1{checkout_id}" >+ }, > "/patrons": { > "$ref": "paths/patrons.json#/~1patrons" > }, >diff --git a/api/v1/swagger/paths/checkouts.json b/api/v1/swagger/paths/checkouts.json >new file mode 100644 >index 0000000..cabe92b >--- /dev/null >+++ b/api/v1/swagger/paths/checkouts.json >@@ -0,0 +1,99 @@ >+{ >+ "/checkouts": { >+ "get": { >+ "operationId": "listCheckouts", >+ "tags": ["patrons", "checkouts"], >+ "parameters": [{ >+ "$ref": "../parameters.json#/borrowernumberQueryParam" >+ }], >+ "produces": [ >+ "application/json" >+ ], >+ "responses": { >+ "200": { >+ "description": "A list of checkouts", >+ "schema": { >+ "$ref": "../definitions.json#/checkouts" >+ } >+ }, >+ "403": { >+ "description": "Access forbidden", >+ "schema": { "$ref": "../definitions.json#/error" } >+ }, >+ "404": { >+ "description": "Patron not found", >+ "schema": { >+ "$ref": "../definitions.json#/error" >+ } >+ } >+ }, >+ "x-koha-authorization": { >+ "allow-owner": true, >+ "allow-guarantor": true, >+ "permissions": { >+ "circulate": "circulate_remaining_permissions" >+ } >+ } >+ } >+ }, >+ "/checkouts/{checkout_id}": { >+ "get": { >+ "operationId": "getCheckout", >+ "tags": ["patrons", "checkouts"], >+ "parameters": [{ >+ "$ref": "../parameters.json#/checkoutIdPathParam" >+ }], >+ "produces": ["application/json"], >+ "responses": { >+ "200": { >+ "description": "Updated borrower's checkout", >+ "schema": { "$ref": "../definitions.json#/checkout" } >+ }, >+ "403": { >+ "description": "Access forbidden", >+ "schema": { "$ref": "../definitions.json#/error" } >+ }, >+ "404": { >+ "description": "Checkout not found", >+ "schema": { "$ref": "../definitions.json#/error" } >+ } >+ }, >+ "x-koha-authorization": { >+ "allow-owner": true, >+ "allow-guarantor": true, >+ "permissions": { >+ "circulate": "circulate_remaining_permissions" >+ } >+ } >+ }, >+ "put": { >+ "operationId": "renewCheckout", >+ "tags": ["patrons", "checkouts"], >+ "parameters": [{ >+ "$ref": "../parameters.json#/checkoutIdPathParam" >+ }], >+ "produces": ["application/json"], >+ "responses": { >+ "200": { >+ "description": "Updated borrower's checkout", >+ "schema": { "$ref": "../definitions.json#/checkout" } >+ }, >+ "403": { >+ "description": "Cannot renew checkout", >+ "schema": { "$ref": "../definitions.json#/error" } >+ }, >+ "404": { >+ "description": "Checkout not found", >+ "schema": { "$ref": "../definitions.json#/error" } >+ } >+ }, >+ "x-koha-authorization": { >+ "allow-owner": true, >+ "allow-guarantor": true, >+ "permissions": { >+ "circulate": "circulate_remaining_permissions" >+ } >+ } >+ } >+ } >+} >diff --git a/t/db_dependent/api/v1/checkouts.t b/t/db_dependent/api/v1/checkouts.t >new file mode 100644 >index 0000000..5298d7f >--- /dev/null >+++ b/t/db_dependent/api/v1/checkouts.t >@@ -0,0 +1,228 @@ >+#!/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, write to the Free Software Foundation, Inc., >+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. >+ >+use Modern::Perl; >+ >+use Test::More tests => 57; >+use Test::MockModule; >+use Test::Mojo; >+use t::lib::Mocks; >+use t::lib::TestBuilder; >+ >+use DateTime; >+use MARC::Record; >+ >+use C4::Context; >+use C4::Biblio; >+use C4::Circulation; >+use C4::Items; >+ >+use Koha::Database; >+use Koha::Patron; >+ >+my $schema = Koha::Database->schema; >+$schema->storage->txn_begin; >+my $dbh = C4::Context->dbh; >+my $builder = t::lib::TestBuilder->new; >+$dbh->{RaiseError} = 1; >+ >+$ENV{REMOTE_ADDR} = '127.0.0.1'; >+my $t = Test::Mojo->new('Koha::REST::V1'); >+ >+$dbh->do('DELETE FROM issues'); >+$dbh->do('DELETE FROM items'); >+$dbh->do('DELETE FROM issuingrules'); >+my $loggedinuser = $builder->build({ source => 'Borrower' }); >+ >+$dbh->do(q{ >+ INSERT INTO user_permissions (borrowernumber, module_bit, code) >+ VALUES (?, 1, 'circulate_remaining_permissions') >+}, undef, $loggedinuser->{borrowernumber}); >+ >+my $session = C4::Auth::get_session(''); >+$session->param('number', $loggedinuser->{ borrowernumber }); >+$session->param('id', $loggedinuser->{ userid }); >+$session->param('ip', '127.0.0.1'); >+$session->param('lasttime', time()); >+$session->flush; >+ >+my $patron = $builder->build({ source => 'Borrower', value => { flags => 0 } }); >+my $borrowernumber = $patron->{borrowernumber}; >+my $patron_session = C4::Auth::get_session(''); >+$patron_session->param('number', $borrowernumber); >+$patron_session->param('id', $patron->{ userid }); >+$patron_session->param('ip', '127.0.0.1'); >+$patron_session->param('lasttime', time()); >+$patron_session->flush; >+ >+my $branchcode = $builder->build({ source => 'Branch' })->{ branchcode }; >+my $module = new Test::MockModule('C4::Context'); >+$module->mock('userenv', sub { { branch => $branchcode } }); >+ >+my $tx = $t->ua->build_tx(GET => "/api/v1/checkouts?borrowernumber=$borrowernumber"); >+$tx->req->cookies({name => 'CGISESSID', value => $session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is([]); >+ >+my $notexisting_borrowernumber = $borrowernumber + 1; >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts?borrowernumber=$notexisting_borrowernumber"); >+$tx->req->cookies({name => 'CGISESSID', value => $session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is([]); >+ >+my $biblionumber = create_biblio('RESTful Web APIs'); >+my $itemnumber1 = create_item($biblionumber, 'TEST000001'); >+my $itemnumber2 = create_item($biblionumber, 'TEST000002'); >+my $itemnumber3 = create_item($biblionumber, 'TEST000003'); >+ >+my $date_due = DateTime->now->add(weeks => 2); >+my $issue1 = C4::Circulation::AddIssue($patron, 'TEST000001', $date_due); >+my $date_due1 = Koha::DateUtils::dt_from_string( $issue1->date_due ); >+my $issue2 = C4::Circulation::AddIssue($patron, 'TEST000002', $date_due); >+my $date_due2 = Koha::DateUtils::dt_from_string( $issue2->date_due ); >+my $issue3 = C4::Circulation::AddIssue($loggedinuser, 'TEST000003', $date_due); >+my $date_due3 = Koha::DateUtils::dt_from_string( $issue3->date_due ); >+ >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts?borrowernumber=$borrowernumber"); >+$tx->req->cookies({name => 'CGISESSID', value => $session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is('/0/borrowernumber' => $borrowernumber) >+ ->json_is('/0/itemnumber' => $itemnumber1) >+ ->json_is('/0/date_due' => $date_due1->ymd . ' ' . $date_due1->hms) >+ ->json_is('/1/borrowernumber' => $borrowernumber) >+ ->json_is('/1/itemnumber' => $itemnumber2) >+ ->json_is('/1/date_due' => $date_due2->ymd . ' ' . $date_due2->hms) >+ ->json_hasnt('/2'); >+ >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/".$issue3->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $patron_session->id}); >+$t->request_ok($tx) >+ ->status_is(403) >+ ->json_is({ error => "Authorization failure. Missing required permission(s).", >+ required_permissions => { circulate => "circulate_remaining_permissions" } >+ }); >+ >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts?borrowernumber=".$loggedinuser->{borrowernumber}); >+$tx->req->cookies({name => 'CGISESSID', value => $patron_session->id}); >+$t->request_ok($tx) >+ ->status_is(403) >+ ->json_is({ error => "Authorization failure. Missing required permission(s).", >+ required_permissions => { circulate => "circulate_remaining_permissions" } >+ }); >+ >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts?borrowernumber=$borrowernumber"); >+$tx->req->cookies({name => 'CGISESSID', value => $patron_session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is('/0/borrowernumber' => $borrowernumber) >+ ->json_is('/0/itemnumber' => $itemnumber1) >+ ->json_is('/0/date_due' => $date_due1->ymd . ' ' . $date_due1->hms) >+ ->json_is('/1/borrowernumber' => $borrowernumber) >+ ->json_is('/1/itemnumber' => $itemnumber2) >+ ->json_is('/1/date_due' => $date_due2->ymd . ' ' . $date_due2->hms) >+ ->json_hasnt('/2'); >+ >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/" . $issue1->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $patron_session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is('/borrowernumber' => $borrowernumber) >+ ->json_is('/itemnumber' => $itemnumber1) >+ ->json_is('/date_due' => $date_due1->ymd . ' ' . $date_due1->hms) >+ ->json_hasnt('/1'); >+ >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/" . $issue1->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is('/date_due' => $date_due1->ymd . ' ' . $date_due1->hms); >+ >+$tx = $t->ua->build_tx(GET => "/api/v1/checkouts/" . $issue2->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is('/date_due' => $date_due2->ymd . ' ' . $date_due2->hms); >+ >+ >+$dbh->do('DELETE FROM issuingrules'); >+$dbh->do(q{ >+ INSERT INTO issuingrules (categorycode, branchcode, itemtype, renewalperiod, renewalsallowed) >+ VALUES (?, ?, ?, ?, ?) >+}, {}, '*', '*', '*', 7, 1); >+ >+my $expected_datedue = DateTime->now->add(days => 14)->set(hour => 23, minute => 59, second => 0); >+$tx = $t->ua->build_tx(PUT => "/api/v1/checkouts/" . $issue1->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is('/date_due' => $expected_datedue->ymd . ' ' . $expected_datedue->hms); >+ >+$tx = $t->ua->build_tx(PUT => "/api/v1/checkouts/" . $issue3->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $patron_session->id}); >+$t->request_ok($tx) >+ ->status_is(403) >+ ->json_is({ error => "Authorization failure. Missing required permission(s).", >+ required_permissions => { circulate => "circulate_remaining_permissions" } >+ }); >+ >+t::lib::Mocks::mock_preference( "OpacRenewalAllowed", 0 ); >+$tx = $t->ua->build_tx(PUT => "/api/v1/checkouts/" . $issue2->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $patron_session->id}); >+$t->request_ok($tx) >+ ->status_is(403) >+ ->json_is({ error => "Opac Renewal not allowed" }); >+ >+t::lib::Mocks::mock_preference( "OpacRenewalAllowed", 1 ); >+$tx = $t->ua->build_tx(PUT => "/api/v1/checkouts/" . $issue2->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $patron_session->id}); >+$t->request_ok($tx) >+ ->status_is(200) >+ ->json_is('/date_due' => $expected_datedue->ymd . ' ' . $expected_datedue->hms); >+ >+$tx = $t->ua->build_tx(PUT => "/api/v1/checkouts/" . $issue1->issue_id); >+$tx->req->cookies({name => 'CGISESSID', value => $session->id}); >+$t->request_ok($tx) >+ ->status_is(403) >+ ->json_is({ error => 'Renewal not authorized (too_many)' }); >+ >+sub create_biblio { >+ my ($title) = @_; >+ >+ my $record = new MARC::Record; >+ $record->append_fields( >+ new MARC::Field('200', ' ', ' ', a => $title), >+ ); >+ >+ my ($biblionumber) = C4::Biblio::AddBiblio($record, ''); >+ >+ return $biblionumber; >+} >+ >+sub create_item { >+ my ($biblionumber, $barcode) = @_; >+ >+ my $item = { >+ barcode => $barcode, >+ }; >+ >+ my $itemnumber = C4::Items::AddItem($item, $biblionumber); >+ >+ return $itemnumber; >+} >-- >2.1.4
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 13895
:
37146
|
40104
|
40143
|
40588
|
41163
|
41548
|
45817
|
50021
|
51303
|
53816
|
53817
|
53878
|
53889
|
53890
|
53891
|
53901
|
53924
|
54196
|
54685
|
54951
|
56167
|
56506
|
58387
|
69123
|
84765
|
85458
|
85459
|
85460
|
85461
|
85462
|
85463
|
85464
|
85676
|
86120
|
86576
|
86579
|
86580
|
87105
|
87106
|
87107
|
87108
|
87109
|
87110
|
87111
|
87112
|
87113
|
87114
|
87115
|
87200