1package Gantry::Plugins::AjaxCRUD; 2 3use strict; 4use Carp; 5use Data::FormValidator; 6 7use Gantry::Utils::CRUDHelp qw( clean_dates clean_params form_profile ); 8 9use base 'Exporter'; 10 11our @EXPORT_OK = qw( select_multiple_closure ); 12 13#----------------------------------------------------------- 14# Constructor 15#----------------------------------------------------------- 16 17sub new { 18 my $class = shift; 19 my $callbacks = { @_ }; 20 21 unless ( defined $callbacks->{template} ) { 22 $callbacks->{template} = 'form.tt'; 23 } 24 25 return bless $callbacks, $class; 26} 27 28#----------------------------------------------------------- 29# Accessors, so we don't misspell hash keys 30#----------------------------------------------------------- 31 32sub add_action { 33 my $self = shift; 34 35 if ( defined $self->{add_action} ) { 36 return $self->{add_action}; 37 } 38 else { 39 croak 'add_action not defined or misspelled'; 40 } 41} 42 43sub edit_action { 44 my $self = shift; 45 46 if ( defined $self->{edit_action} ) { 47 return $self->{edit_action} 48 } 49 else { 50 croak 'edit_action not defined or misspelled'; 51 } 52} 53 54sub delete_action { 55 my $self = shift; 56 57 if ( defined $self->{delete_action} ) { 58 return $self->{delete_action} 59 } 60 else { 61 croak 'delete_action not defined or misspelled'; 62 } 63} 64 65sub setup_action { 66 my $self = shift; 67 68 if ( defined $self->{setup_action} ) { 69 return $self->{setup_action} 70 } 71 else { 72 croak 'setup_action not defined or misspelled'; 73 } 74} 75 76sub cancel_action { 77 my $self = shift; 78 79 if ( defined $self->{cancel_action} ) { 80 return $self->{cancel_action} 81 } 82 else { 83 croak 'cancel_action not defined or misspelled'; 84 } 85} 86 87sub success_action { 88 my $self = shift; 89 90 if ( defined $self->{success_action} ) { 91 return $self->{success_action} 92 } 93 else { 94 croak 'success_action not defined or misspelled'; 95 } 96} 97 98sub form { 99 my $self = shift; 100 101 if ( defined $self->{form} ) { 102 return $self->{form} 103 } 104 else { 105 croak 'form not defined or misspelled'; 106 } 107} 108 109sub text_descr { 110 my $self = shift; 111 return $self->{text_descr} 112} 113 114sub use_clean_dates { 115 my $self = shift; 116 return $self->{use_clean_dates}; 117} 118 119sub turn_off_clean_params { 120 my $self = shift; 121 return $self->{turn_off_clean_params}; 122} 123 124#----------------------------------------------------------- 125# Methods users call 126#----------------------------------------------------------- 127 128#------------------------------------------------- 129# $self->add( $your_self, { put => 'your', data => 'here' } ) 130#------------------------------------------------- 131sub add { 132 my ( $self, $your_self, $data ) = @_; 133 134 eval { 135 $self->setup_action->( $your_self, $data, 'add', $self->text_descr ); 136 }; # failure means they don't wan this 137 138 my $params = $your_self->get_param_hash(); 139 140 # Redirect if user pressed 'Cancel' 141 if ( $params->{cancel} ) { 142 143 return $self->cancel_action->( $your_self, $data, 'add', 'cancel' ); 144 145 } 146 147 # get and hold the form description 148 my $form = $self->form->( $your_self, $data ); 149 150 # Check form data 151 my $show_form = 0; 152 153 $show_form = 1 if ( keys %{ $params } == 0 ); 154 155 my $results = Data::FormValidator->check( 156 $params, 157 form_profile( $form->{fields} ), 158 ); 159 160 $show_form = 1 if ( $results->has_invalid ); 161 $show_form = 1 if ( $results->has_missing ); 162 163 if ( $show_form ) { 164 # order is important, first put in the form... 165 $your_self->stash->view->form( $form ); 166 167 # ... then add error results 168 if ( $your_self->method eq 'POST' ) { 169 $your_self->stash->view->form->results( $results ); 170 } 171 } 172 else { 173 # remove submit button entry 174 delete $params->{submit}; 175 176 if ( $self->turn_off_clean_params ) { 177 if ( $self->use_clean_dates ) { 178 clean_dates( $params, $form->{ fields } ); 179 } 180 } 181 else { 182 clean_params( $params, $form->{ fields } ); 183 } 184 185 $self->add_action->( $your_self, $params, $data ); 186 187 # move along, we're all done here 188 189 return $self->success_action->( $your_self, $data, 'submit', 'add' ); 190 } 191} # END: add 192 193#------------------------------------------------- 194# $self->edit( $your_self, { put => 'your', data => 'here' } ); 195#------------------------------------------------- 196sub edit { 197 my ( $self, $your_self, $data ) = @_; 198 199 eval { 200 $self->setup_action->( $your_self, $data, 'edit', $self->text_descr ); 201 }; # failure means they don't wan this 202 203 my %params = $your_self->get_param_hash(); 204 205 # Redirect if 'Cancel' 206 if ( $params{cancel} ) { 207 208 return $self->cancel_action->( $your_self, $data, 'cancel', 'edit' ); 209 210 } 211 212 # get and hold the form description 213 my $form = $self->form->( $your_self, $data ); 214 215 croak 'Your form callback gave me nothing' unless defined $form and $form; 216 217 my $show_form = 0; 218 219 $show_form = 1 if ( keys %params == 0 ); 220 221 # Check form data 222 my $results = Data::FormValidator->check( 223 \%params, 224 form_profile( $form->{fields} ), 225 ); 226 227 $show_form = 1 if ( $results->has_invalid ); 228 $show_form = 1 if ( $results->has_missing ); 229 230 # Form has errors 231 if ( $show_form ) { 232 # order matters, get form data first... 233 $your_self->stash->view->form( $form ); 234 235 # ... then overlay with results 236 if ( $your_self->method eq 'POST' ) { 237 $your_self->stash->view->form->results( $results ); 238 } 239 240 } 241 # Form looks good, make update 242 else { 243 244 # remove submit button param 245 delete $params{submit}; 246 247 if ( $self->turn_off_clean_params ) { 248 if ( $self->use_clean_dates ) { 249 clean_dates( \%params, $form->{ fields } ); 250 } 251 } 252 else { 253 clean_params( \%params, $form->{ fields } ); 254 } 255 256 $self->edit_action->( $your_self, \%params, $data ); 257 258 # all done, move along 259 260 return $self->success_action->( $your_self, $data, 'submit', 'edit' ); 261 } 262} # END: edit 263 264#------------------------------------------------- 265# $self->delete( $your_self, $confirm, { other => 'data' } ) 266#------------------------------------------------- 267sub delete { 268 my ( $self, $your_self, $yes, $data ) = @_; 269 270 eval { 271 $self->setup_action->( 272 $your_self, $data, 'delete', $self->text_descr 273 ); 274 }; # failure means they don't wan this 275 276 if ( $your_self->params->{cancel} ) { 277 278 return $self->cancel_action->( $your_self, $data, 'cancel', 'delete' ); 279 280 } 281 282 if ( ( defined $yes ) and ( $yes eq 'yes' ) ) { 283 284 $self->delete_action->( $your_self, $data ); 285 286 # Move along, it's already dead 287 288 return $self->success_action->( $your_self, $data, 'submit', 'delete' ); 289 } 290 else { 291 $your_self->stash->view->form->message ( 292 'Delete ' . $self->text_descr() . '?' 293 ); 294 } 295} 296 297#----------------------------------------------------------- 298# Helper functions offered for export 299#----------------------------------------------------------- 300 301sub select_multiple_closure { 302 my $field_name = shift; 303 my $db_selected = shift; 304 305 return sub { 306 my $id = shift; 307 my $params = shift; 308 309 my @real_keys = grep ! /^\./, keys %{ $params }; 310 311 if ( @real_keys ) { 312 return unless $params->{ $field_name }; 313 my @param_ids = split /\0/, $params->{ $field_name }; 314 foreach my $param_id ( @param_ids ) { 315 return 1 if ( $param_id == $id ); 316 } 317 } 318 else { 319 return $db_selected->{ $id }; 320 } 321 }; 322} 323 3241; 325 326__END__ 327 328=head1 NAME 329 330Gantry::Plugins::AjaxCRUD - helper for AJAX based CRUD work 331 332=head1 SYNOPSIS 333 334 use Gantry::Plugins::AjaxCRUD; 335 336 my $user_crud = Gantry::Plugins::AjaxCRUD->new( 337 add_action => \&user_insert, 338 edit_action => \&user_update, 339 delete_action => \&user_delete, 340 form => \&user_form, 341 setup_action => \&user_setup, 342 cancel_action => \&user_cancel, 343 success_action => \&user_success, 344 text_descr => 'database row description', 345 use_clean_dates => 1, 346 turn_off_clean_params => 1, 347 ); 348 349 sub do_add { 350 my ( $self ) = @_; 351 $user_crud->add( $self, { data => \@_ } ); 352 } 353 354 sub user_insert { 355 my ( $self, $form_params, $data ) = @_; 356 # $data is the value of data from do_add 357 358 my $row = My::Model->create( $params ); 359 $row->dbi_commit(); 360 } 361 362 # Similarly for do_delete 363 364 sub do_delete { 365 my ( $self, $doomed_id, $confirm ) = @_; 366 $user_crud->delete( $self, $confirm, { id => $doomed_id } ); 367 } 368 369 sub user_delete { 370 my ( $self, $data ) = @_; 371 372 my $doomed = My::Model->retrieve( $data->{id} ); 373 374 $doomed->delete; 375 My::Model->dbi_commit; 376 } 377 378 sub user_success { 379 my $self = shift; 380 381 $self->do_main( @_ ); 382 } 383 384 sub user_cancel { 385 my $self = shift; 386 387 $self->do_main( @_ ); 388 } 389 390 sub user_setup { 391 my ( $self, $data, $action, $text_descr ) = @_; 392 393 $self->template_wrapper('nowrapper.tt'); 394 $self->stash->view->template('form.tt'); 395 396 $self->stash->view->title('Add' . $text_descr) 397 if ($action eq 'add'); 398 $self->stash->view->title('Edit' . $text_descr) 399 if ($action eq 'edit'); 400 $self->stash->view->title('Delete' . $text_descr) 401 if ($action eq 'delete'); 402 403 } 404 405=head1 DESCRIPTION 406 407This module is very similar to C<Gantry::Plugins::CRUD>, but it is aimed 408at AJAX based systems. Therefore, it resists all urges to refresh the 409page. This leads to three extra callbacks as shown in the summary above 410and discussed below. 411 412For those who don't know, CRUD is short for CReate, Update, and Delete. 413(Some people include retrieve in this list, but users of Perl ORMs can 414use those for retrievals.) While AJAX stands for Asynchronous JavaScript 415and XML. With varying emphasis on the XML part. 416 417What this all means, is that your application is now being driven from 418the browser and not from the server. So a differant style of CRUD needs to 419be used. 420 421Notice: most plugins export methods into your package, this one does NOT. 422 423This module differs from C<Gantry::Plugins::AutoCRUD> in the same ways 424that C<Gantry::Plugins::CRUD> does. It differs from C<Gantry::Plugins::CRUD> 425in how it responds to requests. This module exists to support AJAX forms. 426As such, it does not do anything which might cause a page refresh by the 427browser. 428 429This module still does basically the same things that CRUD does: 430 431 redispatch to listing page if user presses cancel 432 if form parameters are valid: 433 callback to action method 434 else: 435 if method is POST: 436 add form validation errors 437 (re)display form 438 439And as such is an almost drop in replace for CRUD. 440 441=head1 METHODS 442 443This is an object oriented only module (it doesn't export like the other 444plugins). It has many of the same methods as C<Gantry::Plugins::CRUD> plus 445three extras. 446 447=over 4 448 449=item new 450 451Constructs a new AjaxCRUD helper. Pass in a list of the following callbacks 452and config parameters (similar, but not the same as in CRUD): 453 454=over 4 455 456=item add_action (a code ref) 457 458Same as in CRUD. 459 460Called with: 461 462 your self object 463 hash of form parameters 464 the data you passed to add 465 466Called only when the form parameters are valid. You should insert into the 467database and not die (unless the insert fails, then feel free to die). You 468don't need to change your location, but you may. 469 470=item edit_action (a code ref) 471 472Same as in CRUD. 473 474Called with: 475 476 your self object 477 hash of form parameters 478 the data you passed to edit 479 480Called only when form parameters are valid. You should update and not die 481(unless the update fails, then feel free to die). You don't need to change 482your location, but you may. 483 484=item delete_action (a code ref) 485 486Same as in CRUD. 487 488Called with: 489 490 your self object 491 the data you passed to delete 492 493Called only when the user has confirmed that a row should be deleted. 494You should delete the corresponding row and not die (unless the delete 495fails, then feel free to die). You don't need to change your location, 496but you may. 497 498=item form (a code ref) 499 500Same as in CRUD. 501 502Called with: 503 504 your self object 505 the data you passed to add or edit 506 507This needs to return just like the _form method required by 508C<Gantry::Plugins::AutoCRUD>. See its docs for details. 509The only difference between these is that the AutoCRUD calls 510_form with your self object and the row being edited (during editing) 511whereas this method ALWAYS receives both your self object and the 512data you supplied. 513 514=item setup_action (a code ref) 515 516Called with: 517 518 your self object 519 the data you passed to add, edit or deltet 520 the desired action (add, edit or delete) 521 the text description 522 523This method is called immediately by C<add_action>, C<edit_action>, and 524C<delete_action> to set the forms title and template. The default action 525for CRUD is to use form.tt as the template and to wrap your form with the 526site template. Using the site wrapper will cause a page reload. By exposing 527this default, you can change how this is handled. 528 529In the above example this is done by calling $self->template_wrapper() 530with the template nowrapper.tt. What nowrapper.tt needs to do, depends on 531which AJAX toolkit is being used on the browser. But it could be just as 532simple as the following: 533 534 [% content %] 535 536At this point your form is now just a HTML fragment. 537 538Another example, lets say that your boss has just returned from the latest 539Web Developer conference and is all aglow with the possibilites of an AJAX 540front end. He has deemed that all forms should be rendered on the client 541side and JSON will be used to send the form parameters. What to do? 542Well CPAN to the rescue. Install the TT filter for JSON, along with the 543JSON.pm module. Now create a template named json.tt like this: 544 545 [% USE JSON %] 546 [% view.data.json %] 547 548Change the froms template from form.tt to json.tt and add the following 549statement: 550 551 $self->content_type('application/json'); 552 553You are now sending your form as a JSON datastream. 554 555=item cancel_action (a code ref) 556 557Called with: 558 559 your self object 560 the data you passed to add, edit or deltet 561 the action (add, edit or delete) 562 the user request 563 564Triggered by the user successfully submitting the form. 565This and C<success_action> replaces the redirect callback used by 566C<Gantry::Plugins::CRUD>. They should redispatch directly to a do_* method 567like this: 568 569 sub _my_cancel_action { 570 my $self = shift; 571 572 $self->do_something( @_ ); 573 } 574 575=item success_action (a code ref) 576 577Called with: 578 579 your self object 580 the data you passed to add, edit or delete 581 the action (add, edit or delete) 582 the user request 583 584Just like the C<cancel_action>, but triggered when the user presses the Cancel 585button. 586 587=item text_descr 588 589Same as in CRUD. 590 591The text string used in the page titles and in the delete confirmation 592message. 593 594=item use_clean_dates (optional, defaults to false) 595 596Same as in CRUD. 597 598This is ignored unless you turn_off_clean_params, since it is redundant 599when clean_params is in use. 600 601Make this true if you want your dates cleaned immediately before your 602add and edit callbacks are invoked. 603 604Cleaning sets any false fields marked as dates in the form fields list 605to undef. This allows your ORM to correctly insert them as 606nulls instead of trying to insert them as blank strings (which is fatal, 607at least in PostgreSQL). 608 609For this to work your form fields must have this key: C<<is => 'date'>>. 610 611=item turn_off_clean_params (optional, defaults to false) 612 613Same as in CRUD. 614 615By default, right before an SQL insert or update, the params hash from the 616form is passed through the clean_params routine which sets all non-boolean 617fields which are false to undef. This prevents SQL errors with ORMs that 618can correctly translate blank strings into nulls for non-string types. 619 620If you really don't want this routine, set turn_off_clean_params. If you 621turn it off, you can use_clean_dates, which only sets false dates to undef. 622 623=back 624 625Note that in all cases the submit key is removed from the params hash 626by this module before any callback is made. 627 628=item add 629 630Call this in your do_add on a C<Gantry::Plugins::AjaxCRUD> instance: 631 632 sub do_special_add { 633 my $self = shift; 634 $crud_obj->add( $self, { data => \@_ } ); 635 } 636 637It will die unless you passed the following to the constructor: 638 639 add_action 640 form 641 642=item edit 643 644Call this in your do_edit on a C<Gantry::Plugins::AjaxCRUD> instance: 645 646 sub do_special_edit { 647 my $self = shift; 648 my $id = shift; 649 my $row = Data::Model->retrieve( $id ); 650 $crud_obj->edit( $self, { id => $id, row => $row } ); 651 } 652 653It will die unless you passed the following to the constructor: 654 655 edit_action 656 form 657 658=item delete 659 660Call this in your do_delete on a C<Gantry::Plugins::AjaxCRUD> instance: 661 662 sub do_special_delete { 663 my $self = shift; 664 my $id = shift; 665 my $confirm = shift; 666 $crud_obj->delete( $self, $confirm, { id => $id } ); 667 } 668 669The C<$confirm> argument is yes if the delete should go ahead and anything 670else otherwise. This allows our standard practice of having delete 671urls like this: 672 673 http://somesite.example.com/item/delete/4 674 675which leads to the confirmation form whose submit action is: 676 677 http://somesite.example.com/item/delete/4/yes 678 679which is taken as confirmation. 680 681It will die unless you passed the following to the constructor: 682 683 delete_action 684 685=back 686 687You can pick and choose which CRUD help you want from this module. It is 688designed to give you maximum flexibility, while doing the most repetative 689things in a reasonable way. It is perfectly good use of this module to 690have only one method which calls edit. On the other hand, you might have 691two methods that call edit on two different instances, two methods 692that call add on those same instances and a method that calls delete on 693one of the instances. Mix and match. 694 695=head1 HELPER FUNCTIONS 696 697=over 4 698 699=item select_multiple_closure 700 701If you have a form field of type select_multiple, one of the form.tt keys 702is selected. It wants a sub ref so it can reselect items when the form 703fails to validate. This function will generate the proper sub ref (aka 704closure). 705 706Parameters: 707 form field name 708 hash reference of default selections (usually the ones in the database) 709 710Returns: a closure suitable for immediate use as the selected hash key value 711for a form field of type select_multiple. 712 713=back 714 715=head1 SEE ALSO 716 717 Gantry::Plugins::CRUD (for the same approach with page refreshes) 718 719 Gantry::Plugins::AutoCRUD (for simpler situations) 720 721 Gantry and the other Gantry::Plugins 722 723=head1 AUTHOR 724 725Kevin Esteb 726 727=head1 COPYRIGHT and LICENSE 728 729Copyright (c) 2006, Kevin Esteb 730 731This library is free software; you can redistribute it and/or modify 732it under the same terms as Perl itself, either Perl version 5.8.6 or, 733at your option, any later version of Perl 5 you may have available. 734 735=cut 736