Using Catalyst::Controller::REST with jqGrid: Tutorial with Examples for Newbies
- Introduction
- Get Started!
- Using Catalyst::Controller::REST with Catalyst::View::JSON
- Using Catalyst::Controller::REST without Catalyst::View::JSON
- MySQL to SQLite
- What's Next?
Introduction
This is the second in a series of tutorials and examples for Catalyst Newbies. Myself being "over the hill" I naturally feel a kinship with Newbies, and am not ashamed to admit self-identification as a strange old-fashioned geezer-newbie kind of amalgamation. I mean, anybody who would write code in Assembly Language! We'll just stuff those old registers full of stuff, invoke an interrupt or two and be on our way!
In my first tutorial https://blogs.perl.org/users/j0e/2012/09/using-jqgrid-with-catalyst-tutorial-with-examples-for-newbies.html I created a Catalyst application using MySQL, jqGrid, Catalyst::View::JSON and Catalyst::TraitFor::Controller::jQuery::jqGrid.
This second tutorial starts where the final version of the first one left off, and adds a REST controller that uses Catalyst::View::JSON with Catalyst::Controller::REST. After we get the REST controller to work with the JSON view, we will then make it work without it. The purpose of this tutorial is to show you two more ways to communicate with jqGrid: Using a REST controller with and without a JSON view.
Unbeknownst to you—diligent fellow-newbies, I moved from using MySQL to SQLite in these tutorials. I will show you how easily that was done.
Get Started!
We will continue with the final version from the first tutorial and add a controller to use REST. You may want to copy the MyFirstGrid directory from the final version of the first tutorial, to a new location and use that version for this tutorial.
MyFirstGrid.pm:
package MyFirstGrid;
use Moose;
use namespace::autoclean;
use Catalyst::Runtime 5.80;
use Catalyst qw/
-Debug
ConfigLoader
Static::Simple
/;
extends 'Catalyst';
our $VERSION = '0.01';
__PACKAGE__->config(
name => 'MyFirstGrid',
# Disable deprecated behavior needed by old applications
disable_component_resolution_regex_fallback => 1,
enable_catalyst_header => 1, # Send X-Catalyst header
);
# Specify which stash keys are exposed as a JSON response.
__PACKAGE__->config({
'View::JSON' => {
expose_stash => qw(json_data)
}
});
# Start the application
__PACKAGE__->setup();
1;
root/index.tt:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My First Grid</title>
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/cupertino/jquery-ui-1.8.22.custom.css') %]" />
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/ui.jqgrid.css') %]" />
<style type="text/css">
html, body {
margin: 0;
padding: 0;
font-size: 75%;
}
</style>
<script src="[% c.uri_for('/static/js/jquery-1.7.2.min.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/i18n/grid.locale-en.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/jquery.jqGrid.min.js') %]" type="text/javascript"></script>
<script type="text/javascript">
$(function(){
$("#list").jqGrid({
url:"[% c.uri_for("getdata") %]",
datatype:'json',
mtype:'GET',
colNames:['Inv No', 'Client ID', 'Amount','Tax','Total','Notes'],
colModel :[
{name:'inv_id', index:'inv_id', width:55, editable:false, key:true},
{name:'client_id', index:'client_id', width:55, editable:true, editoptions:{size:10}},
{name:'amount', index:'amount', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'tax', index:'tax', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'total', index:'total', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'note', index:'note', width:150, sortable:false, editable: true, edittype:"textarea", editoptions:{rows:"2",cols:"20"}}
],
pager:'#pager',
rowNum:10,
rowList:[10,20,30],
sortname:'inv_id',
sortorder:'desc',
viewrecords:true,
gridview:true,
caption:'My First Grid',
editurl:"[% c.uri_for("postrow") %]"
});
jQuery("#list").jqGrid('navGrid','#pager',
{}, //options
{height:280,reloadAfterSubmit:false}, // edit options
{height:280,reloadAfterSubmit:false}, // add options
{reloadAfterSubmit:false}, // del options
{} // search options
);
});
</script>
</head>
<body>
<table id="list"><tr><td/></tr></table>
<div id="pager"></div>
</body>
</html>
Controller/Root.pm:
package MyFirstGrid::Controller::Root;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' }
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid';
__PACKAGE__->config(namespace => '');
sub index :Path :Args(0) {
my ($self, $c) = @_;
$c->detach($c->view("TT"));
}
sub getdata :Local {
my ($self, $c) = @_;
my $inv_rs = $c->model('DB::Inventory')->search({});
$inv_rs = $self->jqgrid_page($c, $inv_rs);
my @row_data;
while (my $inv = $inv_rs->next) {
my $single_row = {
cell => [
$inv->inv_id,
$inv->client_id,
$inv->amount,
$inv->tax,
$inv->total,
$inv->note,
],
};
push @row_data, $single_row;
}
$c->stash->{json_data}{rows} = \@row_data;
$c->stash->{current_view} = 'JSON';
}
sub postrow :Local {
my ($self, $c) = @_;
my $data = $c->request->parameters;
if ($data->{oper} eq 'edit') { # save row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->update({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'add') { # add new row
my $inv_rs = $c->model('DB::Inventory');
$inv_rs->create({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'del') { # delete row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->delete();
$c->response->status(204);
}
else {
$c->response->body('400 BAD REQUEST: Root/postrow');
$c->response->status(400); # 400 BAD REQUEST
}
}
sub default :Path {
my ($self, $c) = @_;
$c->response->body('Page not found');
$c->response->status(404);
}
# Attempt to render a view, if needed.
sub end :ActionClass('RenderView') {}
__PACKAGE__->meta->make_immutable;
1;
Using Catalyst::Controller::REST with Catalyst::View::JSON
Use the Catalyst helper to create a new controller:
$ script/myfirstgrid_create.pl controller Rest
Controller/Rest.pm:
package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller';}
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}
__PACKAGE__->meta->make_immutable;
1;
We'll extend Catalyst::Controller::REST with Catalyst::TraitFor::Controller::jQuery::jqGrid:
Controller/Rest.pm:
package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid';
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}
__PACKAGE__->meta->make_immutable;
1;
Go back and refresh your memory about Catalyst::TraitFor::Controller::jQuery::jqGrid if you don't remember how it can help you:
We will copy getdata and postrow from the Root controller and use them here in the REST controller. Then we will update url and editurl in our template to use our new REST controller.
Controller/Rest.pm:
package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid';
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}
sub getdata :Local :ActionClass('REST') {}
sub getdata_GET {
my ($self, $c) = @_;
my $inv_rs = $c->model('DB::Inventory')->search({});
$inv_rs = $self->jqgrid_page($c, $inv_rs);
my @row_data;
while (my $inv = $inv_rs->next) {
my $single_row = {
cell => [
$inv->inv_id,
$inv->client_id,
$inv->amount,
$inv->tax,
$inv->total,
$inv->note,
],
};
push @row_data, $single_row;
}
$c->stash->{json_data}{rows} = \@row_data;
$c->stash->{current_view} = 'JSON';
}
sub postrow :Local :ActionClass('REST') {}
sub postrow_POST {
my ($self, $c) = @_;
my $data = $c->request->parameters;
if ($data->{oper} eq 'edit') { # save row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->update({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'add') { # add new row
my $inv_rs = $c->model('DB::Inventory');
$inv_rs->create({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'del') { # delete row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->delete();
$c->response->status(204);
}
else {
$c->response->body('400 BAD REQUEST: Root/postrow');
$c->response->status(400); # 400 BAD REQUEST
}
}
__PACKAGE__->meta->make_immutable;
1;
Update our template to use the REST controller:
root/index.tt:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My First Grid</title>
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/cupertino/jquery-ui-1.8.22.custom.css') %]" />
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/ui.jqgrid.css') %]" />
<style type="text/css">
html, body {
margin: 0;
padding: 0;
font-size: 75%;
}
</style>
<script src="[% c.uri_for('/static/js/jquery-1.7.2.min.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/i18n/grid.locale-en.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/jquery.jqGrid.min.js') %]" type="text/javascript"></script>
<script type="text/javascript">
$(function(){
$("#list").jqGrid({
url:"[% c.uri_for("rest/getdata") %]",
datatype:'json',
mtype:'GET',
colNames:['Inv No', 'Client ID', 'Amount','Tax','Total','Notes'],
colModel :[
{name:'inv_id', index:'inv_id', width:55, editable:false, key:true},
{name:'client_id', index:'client_id', width:55, editable:true, editoptions:{size:10}},
{name:'amount', index:'amount', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'tax', index:'tax', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'total', index:'total', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'note', index:'note', width:150, sortable:false, editable: true, edittype:"textarea", editoptions:{rows:"2",cols:"20"}}
],
pager:'#pager',
rowNum:10,
rowList:[10,20,30],
sortname:'inv_id',
sortorder:'desc',
viewrecords:true,
gridview:true,
caption:'My First Grid',
editurl:"[% c.uri_for("rest/postrow") %]"
});
jQuery("#list").jqGrid('navGrid','#pager',
{}, //options
{height:280,reloadAfterSubmit:false}, // edit options
{height:280,reloadAfterSubmit:false}, // add options
{reloadAfterSubmit:false}, // del options
{} // search options
);
});
</script>
</head>
<body>
<table id="list"><tr><td/></tr></table>
<div id="pager"></div>
</body>
</html>
Run it:
$ script/myfirstgrid_server.pl -d -r -k
Open your browser:
http://0.0.0.0:3000
If your results are the same as mine, you will get an empty grid with no data.
Ouch, what went wrong?
This is where my skill-level as a Newbie is confirmed: I know how to fix this, but I don't clearly understand why. For some reason the last line of getdata_GET is causing our problem:
$c->stash->{current_view} = 'JSON';
The Catalyst console gives:
[debug] "GET" request for "rest/getdata" from "127.0.0.1"
[debug] Query Parameters are:
.-------------------------------------+--------------------------------------.
| Parameter | Value |
+-------------------------------------+--------------------------------------+
| _search | false |
| nd | 1347648333373 |
| page | 1 |
| rows | 10 |
| sidx | inv_id |
| sord | desc |
'-------------------------------------+--------------------------------------'
[debug] Serializing with Catalyst::Action::Serialize::JSON
[debug] Response Code: 400; Content-Type: text/plain; Content-Length: 182
[info] Request took 0.102985s (9.710/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /rest/begin | 0.002102s |
| /rest/getdata | 0.000257s |
| /getdata_GET | 0.069467s |
| /rest/end | 0.020478s |
'------------------------------------------------------------+-----------'
For some reason we are no longer rendering our JSON view.
Changing the last line of getdata_GET to explicitly render the view fixes it:
$c->detach( $c->view("JSON") );
Run it and we get data in the grid, and a little different feedback from the console:
[debug] "GET" request for "rest/getdata" from "127.0.0.1"
[debug] Query Parameters are:
.-------------------------------------+--------------------------------------.
| Parameter | Value |
+-------------------------------------+--------------------------------------+
| _search | false |
| nd | 1347649047150 |
| page | 1 |
| rows | 10 |
| sidx | inv_id |
| sord | desc |
'-------------------------------------+--------------------------------------'
[debug] Response Code: 200; Content-Type: application/json; charset=utf-8; Content-Length: 644
[info] Request took 0.094290s (10.606/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /rest/begin | 0.000499s |
| /rest/getdata | 0.000187s |
| /getdata_GET | 0.087410s |
| -> MyFirstGrid::View::JSON->process | 0.001557s |
| /rest/end | 0.000208s |
'------------------------------------------------------------+-----------'
It seems for some reason our JSON view had not been getting rendered. If you know why, please post a comment with an explanation.
Here is the complete REST controller. It should work correctly:
Controller/Rest.pm:
package MyFirstGrid::Controller::Rest;
use Moose;
use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid';
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Rest in Rest.');
}
sub getdata :Local :ActionClass('REST') {}
sub getdata_GET {
my ($self, $c) = @_;
my $inv_rs = $c->model('DB::Inventory')->search({});
$inv_rs = $self->jqgrid_page($c, $inv_rs);
my @row_data;
while (my $inv = $inv_rs->next) {
my $single_row = {
cell => [
$inv->inv_id,
$inv->client_id,
$inv->amount,
$inv->tax,
$inv->total,
$inv->note,
],
};
push @row_data, $single_row;
}
$c->stash->{json_data}{rows} = \@row_data;
# $c->stash->{current_view} = 'JSON';
$c->detach( $c->view("JSON") );
}
sub postrow :Local :ActionClass('REST') {}
sub postrow_POST {
my ($self, $c) = @_;
my $data = $c->request->parameters;
if ($data->{oper} eq 'edit') { # save row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->update({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'add') { # add new row
my $inv_rs = $c->model('DB::Inventory');
$inv_rs->create({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'del') { # delete row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->delete();
$c->response->status(204);
}
else {
$c->response->body('400 BAD REQUEST: Root/postrow');
$c->response->status(400); # 400 BAD REQUEST
}
}
__PACKAGE__->meta->make_immutable;
1;
Using Catalyst::Controller::REST without Catalyst::View::JSON
We are now able to use a JSON view with our REST controller. Next we will create a new REST controller, and let it serialize responses itself rather than with a JSON view.
Save Controller/Rest.pm as Controller/Restnv.pm and change class names accordingly:
Controller/Restnv.pm:
package MyFirstGrid::Controller::Restnv;
use Moose;
use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid';
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}
sub getdata :Local :ActionClass('REST') {}
sub getdata_GET {
my ($self, $c) = @_;
my $inv_rs = $c->model('DB::Inventory')->search({});
$inv_rs = $self->jqgrid_page($c, $inv_rs);
my @row_data;
while (my $inv = $inv_rs->next) {
my $single_row = {
cell => [
$inv->inv_id,
$inv->client_id,
$inv->amount,
$inv->tax,
$inv->total,
$inv->note,
],
};
push @row_data, $single_row;
}
$c->stash->{json_data}{rows} = \@row_data;
# $c->stash->{current_view} = 'JSON';
$c->detach( $c->view("JSON") );
}
sub postrow :Local :ActionClass('REST') {}
sub postrow_POST {
my ($self, $c) = @_;
my $data = $c->request->parameters;
if ($data->{oper} eq 'edit') { # save row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->update({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'add') { # add new row
my $inv_rs = $c->model('DB::Inventory');
$inv_rs->create({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'del') { # delete row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->delete();
$c->response->status(204);
}
else {
$c->response->body('400 BAD REQUEST: Root/postrow');
$c->response->status(400); # 400 BAD REQUEST
}
}
__PACKAGE__->meta->make_immutable;
1;
Update the template to use the new controller:
root/index.tt:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>My First Grid</title>
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/cupertino/jquery-ui-1.8.22.custom.css') %]" />
<link rel="stylesheet" type="text/css" media="screen" href="[% c.uri_for('/static/css/ui.jqgrid.css') %]" />
<style type="text/css">
html, body {
margin: 0;
padding: 0;
font-size: 75%;
}
</style>
<script src="[% c.uri_for('/static/js/jquery-1.7.2.min.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/i18n/grid.locale-en.js') %]" type="text/javascript"></script>
<script src="[% c.uri_for('/static/js/jquery.jqGrid.min.js') %]" type="text/javascript"></script>
<script type="text/javascript">
$(function(){
$("#list").jqGrid({
// url:"[% c.uri_for("getdata") %]",
// url:"[% c.uri_for("rest/getdata") %]",
url:"[% c.uri_for("restnv/getdata") %]",
datatype:'json',
mtype:'GET',
colNames:['Inv No', 'Client ID', 'Amount','Tax','Total','Notes'],
colModel :[
{name:'inv_id', index:'inv_id', width:55, editable:false, key:true},
{name:'client_id', index:'client_id', width:55, editable:true, editoptions:{size:10}},
{name:'amount', index:'amount', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'tax', index:'tax', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'total', index:'total', width:80, align:'right', editable:true, editoptions:{size:10}},
{name:'note', index:'note', width:150, sortable:false, editable: true, edittype:"textarea", editoptions:{rows:"2",cols:"20"}}
],
pager:'#pager',
rowNum:10,
rowList:[10,20,30],
sortname:'inv_id',
sortorder:'desc',
viewrecords:true,
gridview:true,
caption:'My First Grid',
// editurl:"[% c.uri_for("postrow") %]"
// editurl:"[% c.uri_for("rest/postrow") %]"
editurl:"[% c.uri_for("restnv/postrow") %]"
});
jQuery("#list").jqGrid('navGrid','#pager',
{}, //options
{height:280,reloadAfterSubmit:false}, // edit options
{height:280,reloadAfterSubmit:false}, // add options
{reloadAfterSubmit:false}, // del options
{} // search options
);
});
</script>
</head>
<body>
<table id="list"><tr><td/></tr></table>
<div id="pager"></div>
</body>
</html>
Run it and the new controller should be working perfectly. You should be able to add, edit and delete rows with no problems, after all the new Restnv controller is identical to the Rest controller except for it's name: They both use Catalyst::Controller::REST with a JSON view.
How do we make the Restnv controller not use the JSON view?
The only place we render a JSON view in the Restnv controller is in the last line of the getrow_GET method:
$c->detach( $c->view("JSON") );
So let's change it to one of the Catalyst::Controller::REST Status Helpers in it's documentation:
status_ok
: Returns a "200 OK" response. Takes an "entity" to serialize.status_created
: Returns a "201 CREATED" response. Takes an "entity" to serialize, and a "location" where the created object can be found.status_accepted
: Returns a "202 ACCEPTED" response. Takes an "entity" to serialize. Also takes optional "location" for queue type scenarios.status_no_content
: Returns a "204 NO CONTENT" response.status_multiple_choices
: Returns a "300 MULTIPLE CHOICES" response. Takes an "entity" to serialize, which should provide list of possible locations. Also takes optional "location" for preferred choice.status_found
: Returns a "302 FOUND" response. Takes an "entity" to serialize. Also takes optional "location".status_bad_request
: Returns a "400 BAD REQUEST" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.status_forbidden
: Returns a "403 FORBIDDEN" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.status_not_found
: Returns a "404 NOT FOUND" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.gone
: Returns a "41O GONE" response. Takes a "message" argument as a scalar, which will become the value of "error" in the serialized response.
Change getdata_GET
to use the status_ok
helper:
$self->status_ok(
$c,
entity => {
rows => \@row_data,
},
);
Controller/Restnv.pm:
package MyFirstGrid::Controller::Restnv;
use Moose;
use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid';
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}
sub getdata :Local :ActionClass('REST') {}
sub getdata_GET {
my ($self, $c) = @_;
my $inv_rs = $c->model('DB::Inventory')->search({});
$inv_rs = $self->jqgrid_page($c, $inv_rs);
my @row_data;
while (my $inv = $inv_rs->next) {
my $single_row = {
cell => [
$inv->inv_id,
$inv->client_id,
$inv->amount,
$inv->tax,
$inv->total,
$inv->note,
],
};
push @row_data, $single_row;
}
$self->status_ok(
$c,
entity => {
rows => \@row_data,
},
);
}
sub postrow :Local :ActionClass('REST') {}
sub postrow_POST {
my ($self, $c) = @_;
my $data = $c->request->parameters;
if ($data->{oper} eq 'edit') { # save row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->update({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'add') { # add new row
my $inv_rs = $c->model('DB::Inventory');
$inv_rs->create({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'del') { # delete row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->delete();
$c->response->status(204);
}
else {
$c->response->body('400 BAD REQUEST: Root/postrow');
$c->response->status(400); # 400 BAD REQUEST
}
}
__PACKAGE__->meta->make_immutable;
1;
Run it and the grid looks good. Data is displayed and you can edit, add and delete rows—you can do everything except view the next page in the grid.
Oops!
I discovered what is wrong by using my browser's developer tools to look at my GET response. First I looked at my current response:
{"rows":[{"cell":[37,2112,139.98,81.34,221.32,"This is record 37."]},{"cell":[36,3632,138.98,80.34,219.32,"This is record 36."]},{"cell":[35,8471,137.98,79.34,217.32,"This is record 35."]},{"cell":[34,7377,136.98,78.34,215.32,"This is record 34."]},{"cell":[33,8221,135.98,77.34,213.32,"This is record 33."]},{"cell":[32,2985,134.98,76.34,211.32,"This is record 32."]},{"cell":[31,9483,133.98,75.34,209.32,"This is record 31."]},{"cell":[30,5232,132.98,74.34,207.32,"This is record 30."]},{"cell":[29,6483,131.98,73.34,205.32,"This is record 29."]},{"cell":[28,2232,130.98,72.34,203.32,"This is record 28."]}]}
Then I compared this to a response that was working earlier:
{"page":"1","records":37,"rows":[{"cell":[37,2112,139.98,81.34,221.32,"This is record 37."]},{"cell":[36,3632,138.98,80.34,219.32,"This is record 36."]},{"cell":[35,8471,137.98,79.34,217.32,"This is record 35."]},{"cell":[34,7377,136.98,78.34,215.32,"This is record 34."]},{"cell":[33,8221,135.98,77.34,213.32,"This is record 33."]},{"cell":[32,2985,134.98,76.34,211.32,"This is record 32."]},{"cell":[31,9483,133.98,75.34,209.32,"This is record 31."]},{"cell":[30,5232,132.98,74.34,207.32,"This is record 30."]},{"cell":[29,6483,131.98,73.34,205.32,"This is record 29."]},{"cell":[28,2232,130.98,72.34,203.32,"This is record 28."]}],"total":4}
Ahh, I am currently not providing the following:
"page":"1","records":37,"total":4
The Catalyst::TraitFor::Controller::jQuery::jqGrid docs state the following:
The jqgrid_page method puts the 'page', 'total' and 'records' information onto the stash.
Ahh-ha, the JSON view uses the stash but Catalyst::Controller::REST does not! We have to change this in getdata_GET:
Change this:
$self->status_ok(
$c,
entity => {
rows => \@row_data,
},
);
To this:
$self->status_ok($c, entity => {
page => $c->stash->{json_data}{page},
records => $c->stash->{json_data}{records},
total => $c->stash->{json_data}{total},
rows => \@row_data,
});
Run it and you will find that everything works perfectly, our response is correct and we can page through our data on the grid:
{"page":"1","records":37,"rows":[{"cell":[37,2112,139.98,81.34,221.32,"This is record 37."]},{"cell":[36,3632,138.98,80.34,219.32,"This is record 36."]},{"cell":[35,8471,137.98,79.34,217.32,"This is record 35."]},{"cell":[34,7377,136.98,78.34,215.32,"This is record 34."]},{"cell":[33,8221,135.98,77.34,213.32,"This is record 33."]},{"cell":[32,2985,134.98,76.34,211.32,"This is record 32."]},{"cell":[31,9483,133.98,75.34,209.32,"This is record 31."]},{"cell":[30,5232,132.98,74.34,207.32,"This is record 30."]},{"cell":[29,6483,131.98,73.34,205.32,"This is record 29."]},{"cell":[28,2232,130.98,72.34,203.32,"This is record 28."]}],"total":4}
Controller/Restnv:
package MyFirstGrid::Controller::Restnv;
use Moose;
use namespace::autoclean;
BEGIN {extends 'Catalyst::Controller::REST'}
with 'Catalyst::TraitFor::Controller::jQuery::jqGrid';
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}
sub getdata :Local :ActionClass('REST') {}
sub getdata_GET {
my ($self, $c) = @_;
my $inv_rs = $c->model('DB::Inventory')->search({});
$inv_rs = $self->jqgrid_page($c, $inv_rs);
my @row_data;
while (my $inv = $inv_rs->next) {
my $single_row = {
cell => [
$inv->inv_id,
$inv->client_id,
$inv->amount,
$inv->tax,
$inv->total,
$inv->note,
],
};
push @row_data, $single_row;
}
$self->status_ok($c, entity => {
page => $c->stash->{json_data}{page},
records => $c->stash->{json_data}{records},
total => $c->stash->{json_data}{total},
rows => \@row_data,
});
}
sub postrow :Local :ActionClass('REST') {}
sub postrow_POST {
my ($self, $c) = @_;
my $data = $c->request->parameters;
if ($data->{oper} eq 'edit') { # save row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->update({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'add') { # add new row
my $inv_rs = $c->model('DB::Inventory');
$inv_rs->create({
client_id => $data->{client_id},
amount => $data->{amount},
tax => $data->{tax},
total => $data->{total},
note => $data->{note},
});
$c->response->status(204);
}
elsif ($data->{oper} eq 'del') { # delete row
my $inv_rs = $c->model('DB::Inventory')->search({inv_id => $data->{id}});
$inv_rs->delete();
$c->response->status(204);
}
else {
$c->response->body('400 BAD REQUEST: Root/postrow');
$c->response->status(400); # 400 BAD REQUEST
}
}
__PACKAGE__->meta->make_immutable;
1;
MySQL to SQLite
As mentioned previously, this tutorial uses SQLite rather than MySQL. I will explain how you can use it too.
Create a SQLite database file:
$ sqlite3 myfirstgrid.db < myfirstgrid.sql
This creates a datebase file (myfirstgrid.db) from myfirstgrid.sql:
myfirstgrid.sql:
CREATE TABLE inventory (
inv_id integer primary key autoincrement,
client_id integer not null,
amount real not null default '0.00',
tax real not null default '0.00',
total real not null default '0.00',
note text default null
);
INSERT INTO inventory VALUES (1,2339,103.98,45.34,149.32,"This is record 1.");
INSERT INTO inventory VALUES (2,1171,104.98,46.34,151.32,"This is record 2.");
INSERT INTO inventory VALUES (3,5232,105.98,47.34,153.32,"This is record 3.");
INSERT INTO inventory VALUES (4,9110,106.98,48.34,155.32,"This is record 4.");
INSERT INTO inventory VALUES (5,5232,107.98,49.34,157.32,"This is record 5.");
INSERT INTO inventory VALUES (6,8471,108.98,50.34,159.32,"This is record 6.");
INSERT INTO inventory VALUES (7,2339,109.98,51.34,161.32,"This is record 7.");
INSERT INTO inventory VALUES (8,8471,110.98,52.34,163.32,"This is record 8.");
INSERT INTO inventory VALUES (9,3632,111.98,53.34,165.32,"This is record 9.");
INSERT INTO inventory VALUES (10,6483,112.98,54.34,167.32,"This is record 10.");
INSERT INTO inventory VALUES (11,2333,113.98,55.34,169.32,"This is record 11.");
INSERT INTO inventory VALUES (12,9110,114.98,56.34,171.32,"This is record 12.");
INSERT INTO inventory VALUES (13,1005,115.98,57.34,173.32,"This is record 13.");
INSERT INTO inventory VALUES (14,9987,116.98,58.34,175.32,"This is record 14.");
INSERT INTO inventory VALUES (15,6483,117.98,59.34,177.32,"This is record 15.");
INSERT INTO inventory VALUES (16,5249,118.98,60.34,179.32,"This is record 16.");
INSERT INTO inventory VALUES (17,2333,119.98,61.34,181.32,"This is record 17.");
INSERT INTO inventory VALUES (18,1171,120.98,62.34,183.32,"This is record 18.");
INSERT INTO inventory VALUES (19,1005,121.98,63.34,185.32,"This is record 19.");
INSERT INTO inventory VALUES (20,7377,122.98,64.34,187.32,"This is record 20.");
INSERT INTO inventory VALUES (21,1171,123.98,65.34,189.32,"This is record 21.");
INSERT INTO inventory VALUES (22,5232,124.98,66.34,191.32,"This is record 22.");
INSERT INTO inventory VALUES (23,9110,125.98,67.34,193.32,"This is record 23.");
INSERT INTO inventory VALUES (24,1728,126.98,68.34,195.32,"This is record 24.");
INSERT INTO inventory VALUES (25,4732,127.98,69.34,197.32,"This is record 25.");
INSERT INTO inventory VALUES (26,8221,128.98,70.34,199.32,"This is record 26.");
INSERT INTO inventory VALUES (27,9110,129.98,71.34,201.32,"This is record 27.");
INSERT INTO inventory VALUES (28,2232,130.98,72.34,203.32,"This is record 28.");
INSERT INTO inventory VALUES (29,6483,131.98,73.34,205.32,"This is record 29.");
INSERT INTO inventory VALUES (30,5232,132.98,74.34,207.32,"This is record 30.");
INSERT INTO inventory VALUES (31,9483,133.98,75.34,209.32,"This is record 31.");
INSERT INTO inventory VALUES (32,2985,134.98,76.34,211.32,"This is record 32.");
INSERT INTO inventory VALUES (33,8221,135.98,77.34,213.32,"This is record 33.");
INSERT INTO inventory VALUES (34,7377,136.98,78.34,215.32,"This is record 34.");
INSERT INTO inventory VALUES (35,8471,137.98,79.34,217.32,"This is record 35.");
INSERT INTO inventory VALUES (36,3632,138.98,80.34,219.32,"This is record 36.");
INSERT INTO inventory VALUES (37,2112,139.98,81.34,221.32,"This is record 37.");
I don't know if it is necessary, but I deleted the lib/MyFirstGrid/Model and lib/MyFirstGrid/Schema directories before using the helper to recreate them for our new SQLite database. I copied myfirstgrid.db to MyFirstGrid/myfirstgrid.db. Here is the helper command to recreate the Model and Schema:
$ script/myfirstgrid_create.pl model DB DBIC::Schema \
MyFirstGrid::Schema::DB create=static dbi:SQLite:myfirstgrid.db
What's Next?
Catalyst::Plugin::AutoCRUD
Hi! Thanks for posting great tutorials. I was trying to follow this one, and I seem to have gotten lost. My index page, which should show the Jqgrid is just rendering these characters:
--- ~
I wanted to ask if this is the working method to display to the index page:
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->response->body('Matched MyFirstGrid::Controller::Restnv in Rest.');
}
Thanks!
I am glad you are getting some use from my tutorials.
No, the index method in both the Rest.pm and Restnv.pm controllers are not used, the index method in the Root.pm controller is used.
Sorry for the slow response, I've enabled email notification and should now be able to respond more quickly.
Please let me know how you are doing, I would be happy to answer anymore questions you may have. I can also get on irc.perl.org to help you if we arrange a time to do so.
I was still having issues configuring the application for REST. I have a couple of questions that I hope you can help me with:
1) Are you still using the View::JSON config in your MyFirstGrid.pm file
2) Loading the index page is showing as:
-- ~
from the logs, the index page was not properly loaded by the server:
[debug] Serializing with Catalyst::Action::Serialize::YAML::HTML
[debug] Response Code: 200; Content-Type: text/html; Content-Length: 67
[info] Request took 0.014918s (67.033/s)
Thanks again for your help!
1) When using the Rest.pm controller you need to use the View::JSON config in MyFirstGrid.pm, but you don't need to do so when using the Restnv.pm controller.
2) When loading the index page with the Restnv.pm controller, with the View::JSON config commented out in MyFirstGrid.pm, I get the following:
[info] *** Request 1 (0.050/s) [4164] [Fri May 10 00:11:58 2013] ***
[debug] Path is "/"
[debug] "GET" request for "/" from "127.0.0.1"
[debug] Rendering template "index.tt"
[debug] Response Code: 200; Content-Type: text/html; charset=utf-8; Content-Length: 2567
[info] Request took 0.044164s (22.643/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /index | 0.040985s |
| -> MyFirstGrid::View::TT->process | 0.040587s |
| /end | 0.000195s |
'------------------------------------------------------------+-----------'
[info] *** Request 2 (0.100/s) [4164] [Fri May 10 00:11:58 2013] ***
[debug] Path is "restnv/getdata"
[debug] "GET" request for "restnv/getdata" from "127.0.0.1"
[debug] Query Parameters are:
.-------------------------------------+--------------------------------------.
| Parameter | Value |
+-------------------------------------+--------------------------------------+
| _search | false |
| nd | 1368159118605 |
| page | 1 |
| rows | 10 |
| sidx | inv_id |
| sord | desc |
'-------------------------------------+--------------------------------------'
[debug] Serializing with Catalyst::Action::Serialize::JSON
[debug] Response Code: 200; Content-Type: application/json; Content-Length: 644
[info] Request took 0.262893s (3.804/s)
.------------------------------------------------------------+-----------.
| Action | Time |
+------------------------------------------------------------+-----------+
| /restnv/begin | 0.000116s |
| /restnv/getdata | 0.000055s |
| -> restnv/getdata_GET | 0.252447s |
| /restnv/end | 0.007377s |
'------------------------------------------------------------+-----------'
Note that the content-type in the header of my index.tt is text/html, and the jqGrid function sets datatype to 'json'.
As to why you are serializing with Catalyst::Action::Serialize::YAML::HTML? Sorry, I don't have much experience (yet!) with HTTP, I'm not sure why you are seeing this.
You may want to look at the Catalyst::Controller::REST documentation. It states that "It evaluates which serializer to use by mapping a content-type to a Serialization module." It goes on to say "A given serialization mechanism is only available if you have the underlying modules installed." It indicates that it uses the YAML::HTML module for text/html content, and the JSON module for application/json content - but strongly advises to have JSON::XS installed.
I am sorry to say I had just started working with Catalyst and was in over my head when I wrote this tutorial. I got the code to work but had little understanding. My "tutorial" is very poorly written and I apologize for that.
Thank you for your questions, they caused me to think. Musicians used to talk about "going to the woodshed" when they needed to work hard on their skills, likewise I need to go back to the woodshed with this tutorial. Unfortunately I have little time to do so - I'm away from home 65 hours a week taking care of people and lately have had little time for Perl :-(
Hopefully that may soon change :-)
I recently started learning about github, so I attempted to put MyFirstGrid out there for you:
https://github.com/j0eaxe/jqgrid
I'm sorry that I don't have a better response to your questions. Please continue to ask them and I'll do my best to answer. I'll be off work today (Friday, May 10th) and plan to be on #catalyst at irc.perl.org from about 8 AM to about 10 PM east-coast USA time, my nickname is j0e.
Your last response helped a lot. I finally figured it out. The index action I was using was within a REST controller, which is why the response is in YAML. I just had to move it to a non REST controller for it to work.
Thanks again for this great tutorial!