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