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 L<Jifty::Action>s that are
merely updating L<Jifty::Record> objects.  To use it, subclass it and
override the C<record_class> method to return the name of the
L<Jifty::Record> 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<Jifty::Action::Record/arguments> method to further
specify that all of the primary keys B<must> have values when
submitted; that is, they are
L<constructors|Jifty::Manual::Glossary/constructors>.

=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<submitted> -- 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<Jifty::Action>, where mandatory arguments B<must> 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<take_action> method on L<Jifty::Action> to
call the appropriate C<Jifty::Record>'s C<set_> methods when the
action is run, thus updating the object in the database.

=cut

sub take_action {
    my $self = shift;
    my $changed = 0;

    # Prepare the event for later publishing
    my $event_info = $self->_setup_event_before_action();

    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
        # <Sartak> ah ha. I think I know why passing due => undef reports
        #          action success
        # <Sartak> Jifty::Action::Record::Update compares the value of the
        #          field with what you passed in
        # <Sartak> but since user can't read the field, it returns undef
        # <Sartak> and so: they're both undef, no change, skip this column
        # <Sartak> 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
        # <Sartak> 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
        # <Sartak> 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');

    # Publish the update event
    $self->_setup_event_after_action($event_info);

    return 1;
}

=head2 report_success

Sets the L<Jifty::Result/message> 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<private>
or C<protected>.

=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<Jifty::Action::Record>, L<Jifty::Record>

=head1 LICENSE

Jifty is Copyright 2005-2010 Best Practical Solutions, LLC.
Jifty is distributed under the same terms as Perl itself.

=cut

1;
