use warnings; use strict; package Jifty::Action; =head1 NAME Jifty::Action - The ability to Do Things in the framework =head1 SYNOPSIS package MyApp::Action::Foo; use base qw/MyApp::Action Jifty::Action/; sub take_action { ... } 1; =head1 DESCRIPTION C is the superclass for all actions in Jifty. Action classes form the meat of the L framework; they control how form elements interact with the underlying model. See also L for data-oriented actions, L for how to return values from actions. See Jifty::Action::Schema; =cut use base qw/Jifty::Object Class::Accessor::Fast Class::Data::Inheritable/; __PACKAGE__->mk_accessors(qw(moniker argument_values values_from_request order result sticky_on_success sticky_on_failure)); __PACKAGE__->mk_classdata(qw/PARAMS/); =head1 COMMON METHODS These common methods are designed to =head2 new Construct a new action. Subclasses who need do custom initialization should start with: my $class = shift; my $self = $class->SUPER::new(@_) B; always go through C<< Jifty->web->new_action >>! The arguments that this will be called with include: =over =item moniker The L of the action. Defaults to an autogenerated moniker. =item order An integer that determines the ordering of the action's execution. Lower numbers occur before higher numbers. Defaults to 0. =item arguments A hash reference of default values for the L of the action. Defaults to none. =item sticky_on_failure A boolean value that determines if the form fields are L when the action fails. Defaults to true. =item sticky_on_success A boolean value that determines if the form fields are L when the action succeeds. Defaults to false. =begin private =item request_arguments A hashref of arguments passed in as part of the L. Internal use only. =end private =back =cut sub new { my $class = shift; my $self = bless {}, $class; my %args = ( order => undef, arguments => {}, request_arguments => {}, sticky_on_success => 0, sticky_on_failure => 1, current_user => undef, @_); if ($args{'current_user'}) { $self->current_user($args{current_user}); } else { $self->_get_current_user(); } if ($args{'moniker'}) { $self->moniker($args{'moniker'}); } else { $self->moniker('auto-'.Jifty->web->serial); $self->log->debug("Generating moniker auto-".Jifty->web->serial); } $self->order($args{'order'}); $self->argument_values( { %{ $args{'request_arguments' } }, %{ $args{'arguments'} } } ); # Keep track of whether arguments came from the request, or were # programmatically set elsewhere $self->values_from_request({}); $self->values_from_request->{$_} = 1 for keys %{ $args{'request_arguments' } }; $self->values_from_request->{$_} = 0 for keys %{ $args{'arguments' } }; $self->result(Jifty->web->response->result($self->moniker) || Jifty::Result->new); $self->result->action_class(ref($self)); $self->sticky_on_success($args{sticky_on_success}); $self->sticky_on_failure($args{sticky_on_failure}); return $self; } =head2 arguments B: this API is now deprecated in favour of the declarative syntax offered by L. This method, along with L, is the most commonly overridden method. It should return a hash which describes the L this action takes: { argument_name => {label => "properties go in this hash"}, another_argument => {mandatory => 1} } Each argument listed in the hash will be turned into a L object. For each argument, the hash that describes it is used to set up the L object by calling the keys as methods with the values as arguments. That is, in the above example, Jifty will run code similar to the following: # For 'argument_name' $f = Jifty::Web::Form::Field->new; $f->name( "argument_name" ); $f->label( "Properties go in this hash" ); If an action has parameters that B be passed to it to execute, these should have the L property set. This is separate from the L property, which deal with requiring that the user enter a value for that field. =cut sub arguments { my $self= shift; return($self->PARAMS || {}); } =head2 run This routine, unsurprisingly, actually runs the action. If the result of the action is currently a success (validation did not fail), C calls L, and finally L. If you're writing your own actions, you probably want to override C instead. =cut sub run { my $self = shift; $self->log->debug("Running action ".ref($self) . " " .$self->moniker); unless ($self->result->success) { $self->log->debug("Not taking action, as it doesn't validate"); # dump field warnings and errors to debug log foreach my $what (qw/warnings errors/) { my $f = "field_" . $what; my @r = map { $_ . ": " . $self->result->{$f}->{$_} } grep { $self->result->{$f}->{$_} } keys %{ $self->result->{$f} }; $self->log->debug("Action result $what:\n\t", join("\n\t", @r)) if (@r); } return; } $self->log->debug("Taking action"); $self->take_action; $self->cleanup; } =head2 validate Checks authorization with L, calls C, canonicalizes and validates each argument that was submitted, but doesn't actually call L. The outcome of all of this is stored on the L of the action. =cut sub validate { my $self = shift; $self->check_authorization || return; $self->setup || return; $self->_canonicalize_arguments; $self->_validate_arguments; } =head2 check_authorization Returns true if whoever invoked this action is authorized to perform this action. By default, always returns true. =cut sub check_authorization { 1; } =head2 setup . C is expected to return a true value, or L will skip all other actions. By default, does nothing. =cut sub setup { 1; } =head2 take_action Do whatever the action is supposed to do. This and L are the most commonly overridden methods. By default, does nothing. The return value from this method is NOT returned. (Instead, you should be using the L object to store a result). =cut sub take_action { 1; } =head2 cleanup Perform any action-specific cleanup. By default, does nothing. Runs after L -- whether or not L returns success. =cut sub cleanup { 1; } =head2 moniker Returns the L for this action. =head2 argument_value ARGUMENT [VALUE] Returns the value from the argument with the given name, for this action. If I is provided, sets the value. =cut sub argument_value { my $self = shift; my $arg = shift; if(@_) { $self->values_from_request->{$arg} = 0; $self->argument_values->{$arg} = shift; } return $self->argument_values->{$arg}; } =head2 has_argument ARGUMENT Returns true if the action has been provided with an value for the given argument, including a default_value, and false if none was ever passed in. =cut sub has_argument { my $self = shift; my $arg = shift; return exists $self->argument_values->{$arg}; } =head2 form_field ARGUMENT Returns a L object for this argument. If there is no entry in the L hash that matches the given C, returns C. =cut sub form_field { my $self = shift; my $arg_name = shift; my $mode = $self->arguments->{$arg_name}{'render_mode'}; $mode = 'update' unless $mode && $mode eq 'read'; $self->_form_widget( argument => $arg_name, render_mode => $mode, @_); } =head2 form_value ARGUMENT Returns a L object that renders a display value instead of an editable widget for this argument. If there is no entry in the L hash that matches the given C, returns C. =cut sub form_value { my $self = shift; my $arg_name = shift; $self->_form_widget( argument => $arg_name, render_mode => 'read', @_); } # Generalized helper for the two above sub _form_widget { my $self = shift; my %args = ( argument => undef, render_mode => 'update', @_); my $field = $args{'argument'}; my $arg_name = $field. '!!' .$args{'render_mode'}; if ( not exists $self->{_private_form_fields_hash}{$arg_name} ) { my $field_info = $self->arguments->{$field}; my $sticky = 0; # Check stickiness iff the values came from the request if($self->values_from_request->{$field} && Jifty->web->response->result($self->moniker)) { $sticky = 1 if $self->sticky_on_failure and $self->result->failure; $sticky = 1 if $self->sticky_on_success and $self->result->success; } # $sticky can be overrided per-parameter $sticky = $field_info->{sticky} if defined $field_info->{sticky}; if ($field_info) { # form_fields overrides stickiness of what the user last entered. my $default_value; $default_value = $field_info->{'default_value'} if exists $field_info->{'default_value'}; $default_value = $self->argument_value($field) if $self->has_argument($field) && !$self->values_from_request->{$field}; $self->{_private_form_fields_hash}{$arg_name} = Jifty::Web::Form::Field->new( %$field_info, action => $self, name => $field, sticky => $sticky, sticky_value => $self->argument_value($field), default_value => $default_value, render_mode => $args{'render_mode'}, %args ); } # else $field remains undef else { Jifty->log->warn("$arg_name isn't a valid field for $self"); } } elsif ( $args{render_as} ) { bless $self->{_private_form_fields_hash}{$arg_name}, "Jifty::Web::Form::Field::$args{render_as}"; } return $self->{_private_form_fields_hash}{$arg_name}; } =head2 hidden ARGUMENT VALUE A shortcut for specifying a form field C which should render as a hidden form field, with the default value C. =cut sub hidden { my $self = shift; my ($arg, $value, @other) = @_; $self->form_field( $arg, render_as => 'hidden', default_value => $value, @other); } =head2 order [INTEGER] Gets or sets the order that the action will be run in. This should be an integer, with lower numbers being run first. Defaults to zero. =head2 result [RESULT] Returns the L method associated with this action. If an action with the same moniker existed in the B request, then this contains the results of that action. =head2 register Registers this action as being present, by outputting a snippet of HTML. This expects that an HTML form has already been opened. Note that this is not a guarantee that the action will be run, even if the form is submitted. See L for the definition of "L" actions. Normally, L takes care of calling this when it is needed. =cut sub register { my $self = shift; Jifty->web->out( qq!\n! ); my %args = %{$self->arguments}; while ( my ( $name, $info ) = each %args ) { next unless $info->{'constructor'}; Jifty::Web::Form::Field->new( %$info, action => $self, input_name => $self->fallback_form_field_name($name), sticky => 0, default_value => ($self->argument_value($name) || $info->{'default_value'}), render_as => 'Hidden' )->render(); } return ''; } =head2 render_errors Render any the L of this action, if any, as HTML. Returns nothing. =cut sub render_errors { my $self = shift; if (defined $self->result->error) { # XXX TODO FIXME escape? Jifty->web->out( '
' . '' . $self->result->error . '' . '
' ); } return ''; } =head2 button arguments => { KEY => VALUE }, PARAMHASH Create and render a button. It functions nearly identically like L, except it takes C in addition to C, and defaults to submitting this L. Returns nothing. =cut sub button { my $self = shift; my %args = ( arguments => {}, submit => $self, register => 0, @_); if ($args{register}) { # If they ask us to register the action, do so Jifty->web->form->register_action( $self ); Jifty->web->form->print_action_registration($self->moniker); } elsif ( not Jifty->web->form->printed_actions->{ $self->moniker } ) { # Otherwise, if we're not registered yet, do it in the button my $arguments = $self->arguments; $args{parameters}{ $self->register_name } = ref $self; $args{parameters}{ $self->fallback_form_field_name($_) } = $self->argument_value($_) || $arguments->{$_}->{'default_value'} for grep { $arguments->{$_}{constructor} } keys %{ $arguments }; } $args{parameters}{$self->form_field_name($_)} = $args{arguments}{$_} for keys %{$args{arguments}}; Jifty->web->link(%args); } =head3 return PARAMHASH Creates and renders a button, like L, which additionally defaults to calling the current continuation. Takes an additional argument, C, which can specify a default path to return to if there is no current continuation. =cut sub return { my $self = shift; my %args = (@_); my $continuation = Jifty->web->request->continuation; if (not $continuation and $args{to}) { $continuation = Jifty::Continuation->new(request => Jifty::Request->new(path => $args{to})); } delete $args{to}; $self->button( call => $continuation, %args ); } =head1 NAMING METHODS These methods return the names of HTML form elements related to this action. =head2 register_name Returns the name of the "registration" query argument for this action in a web form. =cut sub register_name { my $self = shift; return 'J:A-' . (defined $self->order ? $self->order . "-" : "") .$self->moniker; } sub _prefix_field { my $self = shift; my ($field_name, $prefix) = @_; return join("-", $prefix, $field_name, $self->moniker); } =head2 form_field_name ARGUMENT Turn one of this action's L into a fully qualified name; takes the name of the field as an argument. =cut sub form_field_name { my $self = shift; return $self->_prefix_field(shift, "J:A:F"); } =head2 fallback_form_field_name ARGUMENT Turn one of this action's L into a fully qualified "fallback" name; takes the name of the field as an argument. This is specifically to support checkboxes, which only show up in the query string if they are checked. Jifty creates a checkbox with the value of L as its name and a value of 1, and a hidden input with the value of L as its name and a value of 0; using this information, L can both determine if the checkbox was present at all in the form, as well as its true value. =cut sub fallback_form_field_name { my $self = shift; return $self->_prefix_field(shift, "J:A:F:F"); } =head2 error_div_id ARGUMENT Turn one of this action's L into the id for the div in which its errors live; takes name of the field as an argument. =cut sub error_div_id { my $self = shift; my $field_name = shift; return 'errors-' . $self->form_field_name($field_name); } =head2 warning_div_id ARGUMENT Turn one of this action's L into the id for the div in which its warnings live; takes name of the field as an argument. =cut sub warning_div_id { my $self = shift; my $field_name = shift; return 'warnings-' . $self->form_field_name($field_name); } =head1 VALIDATION METHODS =head2 argument_names Returns the list of argument names. This information is extracted from L. =cut sub argument_names { my $self = shift; my %arguments = %{ $self->arguments }; return ( sort { (($arguments{$a}->{'sort_order'} ||0 ) <=> ($arguments{$b}->{'sort_order'} || 0)) || (($arguments{$a}->{'name'} || '') cmp ($arguments{$b}->{'name'} ||'' )) || $a cmp $b } keys %arguments ); } =head2 _canonicalize_arguments Canonicalizes each of the L that this action knows about. This is done by calling L for each field described by L. =cut # XXX TODO: This is named with an underscore to prevent infinite # looping with arguments named "argument" or "arguments". We need a # better solution. sub _canonicalize_arguments { my $self = shift; $self->_canonicalize_argument($_) for $self->argument_names; } =head2 _canonicalize_argument ARGUMENT Canonicalizes the value of an L. If the argument has an attribute named B, call the subroutine reference that attribute points points to. If it doesn't have a B attribute, but the action has a C> function, also invoke that function. If neither of those are true, by default canonicalize dates using _canonicalize_date =cut # XXX TODO: This is named with an underscore to prevent infinite # looping with arguments named "argument" or "arguments". We need a # better solution. sub _canonicalize_argument { my $self = shift; my $field = shift; my $field_info = $self->arguments->{$field}; my $value = $self->argument_value($field); my $default_method = 'canonicalize_' . $field; # XXX TODO: Do we really want to skip undef values? return unless defined $value; if ( $field_info->{canonicalizer} and defined &{ $field_info->{canonicalizer} } ) { $value = $field_info->{canonicalizer}->( $self, $value ); } elsif ( $self->can($default_method) ) { $value = $self->$default_method( $value ); } elsif ( defined( $field_info->{render_as} ) && lc( $field_info->{render_as} ) eq 'date') { $value = $self->_canonicalize_date( $value ); } $self->argument_value($field => $value); } =head2 _canonicalize_date DATE Parses and returns the date using L. =cut sub _canonicalize_date { my $self = shift; my $val = shift; return undef unless defined $val and $val =~ /\S/; return undef unless my $obj = Jifty::DateTime->new_from_string($val); return $obj->ymd; } =head2 _validate_arguments Validates the form fields. This is done by calling L for each field described by L =cut # XXX TODO: This is named with an underscore to prevent infinite # looping with arguments named "argument" or "arguments". We need a # better solution. sub _validate_arguments { my $self = shift; $self->_validate_argument($_) for $self->argument_names; return $self->result->success; } =head2 _validate_argument ARGUMENT Validate your form fields. If the field C is mandatory, checks for a value. If the field has an attribute named B, call the subroutine reference validator points to. If the action doesn't have an explicit B attribute, but does have a C> function, invoke that function. =cut # XXX TODO: This is named with an underscore to prevent infinite # looping with arguments named "argument" or "arguments". We need a # better solution. sub _validate_argument { my $self = shift; my $field = shift; return unless $field; $self->log->debug(" validating argument $field"); my $field_info = $self->arguments->{$field}; return unless $field_info; my $value = $self->argument_value($field); if ( !defined $value || !length $value ) { if ( $field_info->{mandatory} ) { return $self->validation_error( $field => "You need to fill in this field" ); } } # If we have a set of allowed values, let's check that out. # XXX TODO this should be a validate_valid_values sub if ( $value && $field_info->{valid_values} ) { unless ( grep $_->{'value'} eq $value, @{ $self->valid_values($field) } ) { return $self->validation_error( $field => _("That doesn't look like a correct value") ); } # ... but still check through a validator function even if it's in the list } my $default_validator = 'validate_' . $field; # Finally, fall back to running a validator sub if ( $field_info->{validator} and defined &{ $field_info->{validator} } ) { return $field_info->{validator}->( $self, $value ); } elsif ( $self->can($default_validator) ) { return $self->$default_validator( $value ); } # If none of the checks have failed so far, then it's ok else { return $self->validation_ok($field); } } =head2 _autocomplete_argument ARGUMENT Get back a list of possible completions for C. The list should either be a list of scalar values or a list of hash references. Each hash reference must have a key named C. There can also additionally be a key named C