use warnings; use strict; package Jifty::Action::Record::Update; =head1 NAME Jifty::Action::Record::Update - Automagic update action =head1 DESCRIPTION This class is used as the base class for Ls that are merely updating L objects. To use it, subclass it and override the C method to return the name of the L subclass that this action should update. =cut use base qw/Jifty::Action::Record/; use Scalar::Util qw/ blessed /; =head1 METHODS =head2 arguments Overrides the L method to further specify that all of the primary keys B have values when submitted; that is, they are L. =cut sub arguments { my $self = shift; my $arguments = $self->SUPER::arguments(@_); # Mark read-only columns for read-only display for my $column ( $self->possible_columns ) { if ( (not $column->writable) and $column->readable ) { $arguments->{$column->name}{'render_mode'} = 'read'; } } # Add the primary keys to constructors and make them mandatory for my $pk (@{ $self->record->_primary_keys }) { $arguments->{$pk}{'constructor'} = 1; $arguments->{$pk}{'mandatory'} = 1; $arguments->{$pk}{'render_mode'} = 'read'; } if ( $self->can('PARAMS') ) { use Jifty::Param::Schema; return Jifty::Param::Schema::merge_params( $arguments, ($self->PARAMS || {}) ); } else { return $arguments; } } =head2 validate_arguments We only need to validate arguments that got B -- thus, a mandatory argument that isn't submitted isn't invalid, as it's not going to change the record. This is opposed to the behavior inherited from L, where mandatory arguments B be present for the action to run. However, constructor arguments are still required. =cut sub _validate_arguments { my $self = shift; # Only validate the arguments given $self->_validate_argument($_) for grep { $self->has_argument($_) or $self->arguments->{$_}->{constructor} } $self->argument_names; return $self->result->success; } =head2 take_action Overrides the virtual C method on L to call the appropriate C's C methods when the action is run, thus updating the object in the database. =cut sub take_action { my $self = shift; my $changed = 0; my $detailed_messages = {}; $self->record->call_trigger('start_update_action'); # Iterate through all the possible arguments for my $field ( $self->argument_names ) { # Skip values that weren't submitted next unless $self->has_argument($field); # Load the column object for the field my $column = $self->record->column($field); # Skip nonexistent fields next unless $column; # Grab the value my $value = $self->argument_value($field); # Boolean and integer fields should be set to NULL if blank. # (This logic should be moved into SB or something.) $value = undef if ( defined $column->type and ( $column->type =~ /^bool/i || $column->type =~ /^int/i ) and defined $value and $value eq '' ); # Skip file uploads if blank next if defined $self->arguments->{$field}{render_as} and lc $self->arguments->{$field}{render_as} eq "upload" and (not defined $value or not ref $value); # Skip fields that have not changed, but only if we can read the field. # This prevents us from getting an $old value that is wrongly undef # when really we are just denied read access. At the same time, it means # we can keep the change checks before checking if we can update. if ( $self->record->current_user_can('read', column => $field) ) { my $old = $self->record->$field; # Handle columns which reference other tables my $col = $self->record->column( $field ); my $by = defined $col->by ? $col->by : 'id'; $old = $old->$by if blessed($old) and $old->isa( 'Jifty::Record' ); # ID is sometimes passed in, we want to ignore it if it doesn't change next if $field eq 'id' and defined $old and defined $value and "$old" eq "$value"; # if both the new and old values are defined and equal, we don't want to change em # XXX TODO "$old" is a cheap hack to scalarize datetime objects next if ( defined $old and defined $value and "$old" eq "$value" ); # If _both_ the values are '' next if ( (not defined $old or not length $old) and ( not defined $value or not length $value )); } # Error on columns we can't update # ah ha. I think I know why passing due => undef reports # action success # Jifty::Action::Record::Update compares the value of the # field with what you passed in # but since user can't read the field, it returns undef # and so: they're both undef, no change, skip this column # and since that's the only column that changed, it'll notice # that every column it did try to update (which is.. none of # them) succeeded # I don't think we can just skip ACLs for reading the column # -- that's a potential security issue. an attacker could try # every value until the action succeeds because nothing changed # it doesn't matter for HM but for other apps it may unless ($self->record->current_user_can('update', column => $field, value => $value)) { $self->result->field_error($field, _('Permission denied')); next; } # Calculate the name of the setter and set; asplode on failure my $setter = "set_$field"; my ( $val, $msg ) = $self->record->$setter( $value ); if ($val) { # Remember that we changed something (if we did) $changed = 1; $detailed_messages->{$field} = $msg; } else { $self->result->field_error($field, $msg || _('Permission denied')); } } # Report success if there's a change and no error, otherwise say nah-thing $self->report_success if $changed and not $self->result->failure; $self->result->content(id => $self->record->id); $self->result->content( detailed_messages => $detailed_messages ) if $self->report_detailed_messages; $self->record->call_trigger('end_update_action'); return 1; } =head2 report_success Sets the L to default success message, "Updated". Override this if you want to report some other more user-friendly result. =cut sub report_success { my $self = shift; $self->result->message(_("Updated")) } =head2 possible_columns Update actions do not provide fields for columns marked as C or C. =cut sub possible_columns { my $self = shift; return grep { not $_->protected } $self->SUPER::possible_columns; } =head2 _extra_validator_args Passes C<< for => 'update' >> to validators. =cut sub _extra_validator_args { return { for => 'update' }; } =head2 _extra_canonicalizer_args Passes C<< for => 'update' >> to canonicalizers. =cut sub _extra_canonicalizer_args { return { for => 'update' }; } =head2 _extra_autocompleter_args Passes C<< for => 'update' >> to autocompleters. =cut sub _extra_autocompleter_args { return { for => 'update' }; } =head1 SEE ALSO L, L =head1 LICENSE Jifty is Copyright 2005-2010 Best Practical Solutions, LLC. Jifty is distributed under the same terms as Perl itself. =cut 1;