The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package CatalystX::CRUD::YUI::Controller;

use warnings;
use strict;
use base qw( CatalystX::CRUD::Controller );
use Carp;
use MRO::Compat;
use mro "c3";

use Data::Dump qw( dump );

__PACKAGE__->mk_accessors(
    qw( autocomplete_columns autocomplete_method
        hide_pk_columns fuzzy_search default_view
        fmt_to_view_map
        )
);

__PACKAGE__->config(
    autocomplete_columns => [],
    autocomplete_method  => undef,
    hide_pk_columns      => 1,
    fuzzy_search         => 0,
    default_view         => 'YUI',
    fmt_to_view_map      => {
        'html' => 'YUI',
        'json' => 'YUI',
        'xls'  => 'Excel',
    },
);

our $VERSION = '0.027';

=head1 NAME

CatalystX::CRUD::YUI::Controller - base controller

=head1 SYNOPSIS

 package MyApp::Controller::Foo;
 use strict;
 
 # ISA order is important
 use base qw(
    CatalystX::CRUD::YUI::Controller
    CatalystX::CRUD::Controller::RHTMLO
 );
 
 __PACKAGE__->config(
    autocomplete_columns => [qw( foo bar )],
    autocomplete_method  => 'foo',
    hide_pk_columns      => 1,
    fuzzy_search         => 0,
    default_view         => 'YUI',
    fmt_to_view_map      => {
        'html' => 'YUI',
        'json' => 'YUI',
        'xls'  => 'Excel',
    },
 );
 
 1;
 

=head1 DESCRIPTION

This is a base controller class for use with CatalystX::CRUD::YUI
applications. It implements URI and internal methods that the
accompanying .tt and .js files rely upon.

B<NOTE: As of version 0.008 this class drops support for the YUI
datatable feature and instead supports the ExtJS LiveGrid feature.>

=head1 CONFIGURATION

See SYNOPSIS for config options specific to this class.

=over

=item autocomplete_columns

See the autocomplete_columns() method. Value should be an array ref.

=item autocomplete_method

See the autocomplete_method() method. Value should be a method name.

=item hide_pk_columns

Used in the LiveGrid class. Boolean setting indicating whether the 
primary key column(s) should appear in the YUI LiveGrid listings
or not. Default is true.

=item fuzzy_search

If true, the C<cxc-fuzzy> param will be appended to all search queries
via the .tt files. See CatalystX::CRUD::Model::Utils for more
documentation about the C<cxc-fuzzy> param.

=item default_view

The name of the View to use if C<cxc-fmt> is not specified. This
should be the name of a View that inherits from CatalystX::CRUD::YUI::View.
The default is 'YUI'.

=item fmt_to_view_map

Hash ref of C<cxc-fmt> types to View names. Used in end() to determine
which View to set.

=back
  
=head1 METHODS

Only new or overridden method are documented here.

=cut

=head2 new

Overrides base method just to call next::method and ensures
config() gets merged correctly.

=cut

sub new {
    my ( $class, $app_class, $args ) = @_;

    my $self = $class->next::method( $app_class, $args );

    if ( $self->isa('CatalystX::CRUD::REST') ) {

        # must merge hashes manually due to inheritance bug with C::D::I
        # this only seems to happen when inheriting also from CXC::REST
        # either because the inheritance chain is too deep or something
        # else mysterious...
        $self->config(
            $self->merge_config_hashes( $self->_config, __PACKAGE__->config )
        );
        for my $key ( keys %{ $self->config } ) {
            next unless $self->can($key);
            next if defined $self->$key;
            $self->$key( $self->config->{$key} );
        }

    }
    return $self;
}

=head2 json_mime

Returns JSON MIME type. Default is 'application/json; charset=utf-8'.

=cut

sub json_mime {'application/json; charset=utf-8'}

=head2 default

Redirects to URI for 'count' in same namespace.

=cut

sub default : Path {
    my ( $self, $c ) = @_;
    $c->response->redirect( $c->uri_for('count') );
}

=head2 livegrid( I<context>, I<arg> )

Public URI method. Returns JSON for ExtJS LiveGrid feature.

=cut

sub livegrid : Local {
    my ( $self, $c, @arg ) = @_;

    # param name compat with cxc api
    # TODO configurable names in livegrid js?
    my $params = $c->req->params;
    $params->{'cxc-dir'}       = $params->{dir};
    $params->{'cxc-sort'}      = $params->{sort};
    $params->{'cxc-page_size'} = $params->{limit};
    $params->{'cxc-offset'}    = $params->{start};

    $c->stash( view_on_single_result => 0 );
    $self->do_search( $c, @arg );
    $c->stash( template => 'crud/livegrid.tt' );
    $c->response->content_type( $self->json_mime );
}

=head2 livegrid_create_form( I<context> )

Returns plain HTML form without wrapper for use with LiveGrid
relationship manager.

=cut

sub livegrid_create_form : Local {
    my ( $self, $c ) = @_;
    $self->create($c);
    $c->stash( no_wrapper => 1 );
    $c->stash( template   => 'crud/create.tt' );
}

=head2 livegrid_edit_form( I<context>, I<oid> ) 

Lke livegrid_create_form but returns form initialized for
record represented by I<oid>. Chained to fetch().

=cut

sub livegrid_edit_form : PathPart Chained('fetch') Args(0) {
    my ( $self, $c ) = @_;
    $self->edit($c);
    $c->stash( no_wrapper => 1 );
    $c->stash( template   => 'crud/edit.tt' );
}

=head2 livegrid_related( I<oid>, I<relationship_name> )

Public URI method. Returns JSON for ExtJS LiveGrid feature.

=cut

sub livegrid_related : PathPart Chained('fetch') Args(1) {
    my ( $self, $c, $rel_name ) = @_;

    # param name compat with cxc api
    # TODO configurable names in livegrid js?
    my $params = $c->req->params;

    #$c->log->debug(Data::Dump::dump $params) if $c->debug;

    $params->{'cxc-dir'}       ||= $params->{dir};
    $params->{'cxc-sort'}      ||= $params->{sort};
    $params->{'cxc-page_size'} ||= $params->{limit};
    $params->{'cxc-offset'}    ||= $params->{start};

    $c->stash( view_on_single_result => 0 );
    $self->do_related_search( $c, $rel_name );
    $c->stash( template => 'crud/livegrid.tt' );
    $c->response->content_type( $self->json_mime );
}

=head2 do_related_search( I<context>, I<relationship_name> )

Sets up stash() to mimic the foreign controller 
represented by I<relationship_name>.

=cut

sub do_related_search {
    my ( $self, $c, $rel_name ) = @_;

    my $obj     = $c->stash->{object};
    my $query   = $self->do_model( $c, 'make_sql_query' );
    my $count   = $self->do_model( $c, 'count_related', $obj, $rel_name );
    my $results = $self->do_model( $c, 'iterator_related', $obj, $rel_name );
    my $pager;
    if ($count) {
        $pager = $self->do_model( $c, 'make_pager', $count, $results );
    }

    $c->stash(
        results => CatalystX::CRUD::Results->new(
            {   count   => $count,
                pager   => $pager,
                results => $results,
                query   => $query,
            }
        )
    );

    # set the controller so we mimic the foreign controller
    my $relinfo = $c->stash->{form}->metadata->relationship_info($rel_name);
    $c->stash(
        controller  => $relinfo->get_controller,
        method_name => $rel_name,
        form        => $relinfo->get_controller->form($c),
        field_names =>
            $relinfo->get_controller->form($c)->metadata->field_methods
    );
}

=head2 remove

Overrides superclass method to set
the content response to 'Ok' on success, 
or a generic error string on failure.

B<CAUTION>: This URI is for ManyToMany only. Using it on OneToMany
or ManyToOne I<rel_name> values will delete the related row altogether.

=cut

sub remove : PathPart Chained('related') Args(0) {
    my ( $self, $c, $rel, $foreign_pk, $foreign_pk_value ) = @_;
    eval { $self->next::method($c) };

    if ( $@ or $self->has_errors($c) ) {
        $c->log->error($@);
        $c->log->error($_) for @{ $c->error };
        $c->clear_errors;
        $c->res->body("Error removing related object");
        $c->res->status(500);
        return;
    }
    else {
        $c->response->body('Ok');
        $c->response->status(200);    # because we are returning content
    }
}

=head2 add

Overrides superclass method to return
the new record as JSON on success, or a generic 
error string on failure.

=cut

sub add : PathPart Chained('related') Args(0) {
    my ( $self, $c ) = @_;

    # pull the newly associated record out and json-ify it for return
    my $obj              = $c->stash->{object};
    my $rel              = $c->stash->{rel_name};
    my $foreign_pk_value = $c->stash->{foreign_pk_value};

    # check first if already defined so we don't try and re-add
    for my $rec ( $obj->$rel ) {
        my $pk = $rec->primary_key_uri_escaped;

        #warn "add compare: $pk <-> $foreign_pk_value";
        if ( $pk eq $foreign_pk_value ) {
            $c->res->body(
                "Related $rel record $foreign_pk_value already associated.");
            $c->res->status(400);
            return;
        }
    }

    eval { $self->next::method($c) };

    if ( $@ or $self->has_errors($c) ) {
        $c->log->error($@);
        $c->log->error($_) for @{ $c->error };
        $c->clear_errors;
        $c->res->body("Error adding related object");
        $c->res->status(500);
        return;
    }

    my $record;
    for my $rec ( $obj->$rel ) {
        my $pk = $rec->primary_key_uri_escaped;
        if ( $pk eq $foreign_pk_value ) {
            $record = $rec;
            last;
        }
    }
    if ( !$record ) {
        $self->throw_error(
            "cannot find newly saved record for $rel $foreign_pk_value");
        return;
    }

    # we want the column names, etc., from the foreign controller's form.
    my $foreign_controller
        = $self->form($c)->metadata->relationship_info($rel)->get_controller;
    my $foreign_form = $foreign_controller->form($c);
    my $foreign_pk   = $foreign_controller->primary_key;

    # list of columns must include PK but that is often not in Form
    # if is an autoincrem value
    my @fields = @{ $foreign_form->metadata->field_methods };
    push( @fields, ref $foreign_pk ? @$foreign_pk : $foreign_pk );
    $c->stash(
        template    => 'crud/jsonify.tt',
        serial_args => {
            object => $record,
            parent => $obj,
            takes_object_as_argument =>
                $foreign_form->metadata->takes_object_as_argument,
            col_names => \@fields,
        }
    );
    $c->response->content_type( $self->json_mime );
    $c->response->status(200);    # because we are returning content
}

=head2 form_to_object

Overrides the base CRUD method to catch errors if the expected
return format is JSON.

=cut

# catch any errs so we can render json if needed
sub form_to_object {
    my ( $self, $c ) = @_;

    #carp "form_to_object";

    # check for o2m via livegrid-click to avoid
    # re-saving duplicates
    if ( $c->req->params->{'cxc-o2m'} ) {
        my $obj      = $c->stash->{object};
        my $match    = 0;
        my $eligible = 0;
        for my $param ( keys %{ $c->req->params } ) {
            if ( $obj->can($param) ) {
                $eligible++;
                if ( $obj->$param eq $c->req->params->{$param} ) {
                    $match++;
                    $c->log->debug( "object already has $param = "
                            . $c->req->params->{$param} )
                        if $c->debug;
                }
            }
        }
        if ( $match == $eligible ) {
            $c->response->body('Object already related');
            $c->response->status(400);
            return;
        }
    }

    my $obj = $self->next::method($c);

    if (   !$obj
        && exists $c->req->params->{'cxc-fmt'}
        && $c->req->params->{'cxc-fmt'} eq 'json' )
    {
        $c->response->status(500);
        my $err = $self->all_form_errors( $c->stash->{form} );
        $err =~ s,\n,<br />,g;
        $c->response->body($err);
    }
    return $obj;
}

=head2 postcommit

Overrides base method to re-read object from db.

=cut

sub postcommit {
    my ( $self, $c, $obj ) = @_;

    $c->log->debug("postcommit YUI") if $c->debug;

    # get whatever auto-set values were set.
    unless ( $c->action->name eq 'rm' ) {
        if ( $self->model_adapter ) {
            $self->model_adapter->read( $c, $obj );
        }
        else {
            $obj->read;
        }
    }

    $self->next::method( $c, $obj );

    return $obj;
}

=head2 autocomplete_columns

Should return arrayref of fields to search when
the autocomplete() URI method is requested.

Set this value in config(). Default is a no-op.

=cut

# this is a no-op by default. subclasses can override it.
# it is marked with the _private prefix for now.
sub _get_autocomplete_columns {
    my ( $self, $c ) = @_;
    return $self->autocomplete_columns;
}

=head2 autocomplete_method

Which method should be called on each search result to create the 
response list.

Default is the first item in autocomplete_columns().

Set this value in config(). Default is a no-op.

=cut

sub _get_autocomplete_method {
    my ( $self, $c ) = @_;
    my $accols = $self->autocomplete_columns
        || $self->_get_autocomplete_columns;

    $self->autocomplete_method( @$accols ? $accols->[0] : undef );
    return $self->autocomplete_method;
}

=head2 autocomplete( I<context> )

Public URI method. Supports the Rose::HTMLx::Form::Field::Autocomplete
API.

=cut

sub autocomplete : Local {
    my ( $self, $c ) = @_;
    if ( !$self->can_read($c) ) {
        $self->throw_error("Permission denied");
        return;
    }
    my $p = $c->req->params;
    unless ( $p->{l} and $p->{c} and $p->{query} ) {
        $self->throw_error("need l and c and query params");
        return;
    }

    my $ac_columns = $self->autocomplete_columns
        || $self->_get_autocomplete_columns($c);
    if ( !@$ac_columns ) {
        $self->throw_error("no autocomplete columns defined");
        return;
    }

    my $ac_method = $self->autocomplete_method
        || $self->_get_autocomplete_method;
    if ( !$ac_method ) {
        $self->throw_error("no autocomplete method defined");
        return;
    }

    #warn "ac_columns: " . dump $ac_columns;
    #warn "ac_method: " . $ac_method;

    $p->{'cxc-fuzzy'}     = 1;
    $p->{'cxc-page_size'} = $p->{l};
    $p->{'cxc-op'}        = 'OR';
    $p->{'cxc-fmt'}       = 'json';
    $p->{$_} = $p->{query} for @$ac_columns;
    my $query = $self->do_model( $c, 'make_query', $ac_columns );

    $c->stash->{results}    = $self->do_model( $c, 'search', $query );
    $c->stash->{ac_field}   = $p->{c};
    $c->stash->{ac_method}  = $ac_method;
    $c->stash->{ac_columns} = $ac_columns;
    $c->stash->{template}   = 'crud/autocomplete.tt';
}

=head2 end

Uses the RenderView ActionClass.

Prior to passing to view, sets C<current_view> if it is not set,
based on the C<cxc-fmt> request parameter, defaulting
to 'YUI'.

B<NOTE:>This assumes you have a View class called
C<YUI> that inherits from CatalystX::CRUD::YUI::View.

=cut

sub end : ActionClass('RenderView') {
    my ( $self, $c, @arg ) = @_;
    my $fmt = $c->req->params->{'cxc-fmt'} || 'html';

    if ($fmt) {
        $c->log->debug("fmt = $fmt") if $c->debug;
        my $view_class = $self->fmt_to_view_map->{$fmt}
            || $self->default_view;
        $c->stash->{current_view} ||= $view_class;

        # special case for add() and save()
        if ( $fmt eq 'json' and defined $c->stash->{object} ) {

            # catch errors and abort so we don't return json
            if ( $self->has_errors($c) ) {
                my @err = @{ $c->error };
                $c->log->error($_) for @err;
                $c->clear_errors;
                $c->response->body('Server error');
                $c->response->status(500);
                return;
            }

            $c->log->debug("JSONifying object for response") if $c->debug;
            $c->stash( template => 'crud/jsonify.tt' )
                unless defined $c->stash->{template};
            my %serial_args = (
                object => $c->stash->{object},
                takes_object_as_argument =>
                    $self->form($c)->metadata->takes_object_as_argument,
                col_names => $self->form($c)->metadata->field_methods,
            );
            if ( $c->stash->{results} ) {
                $serial_args{results} = $c->stash->{results};
                $serial_args{relname} = $c->stash->{rel_name};

         # we want the column names, etc., from the foreign controller's form.
                my $foreign_controller
                    = $self->form($c)
                    ->metadata->relationship_info( $serial_args{relname} )
                    ->get_controller;
                my $foreign_form = $foreign_controller->form($c);
                my $foreign_pk   = $foreign_controller->primary_key;

               # list of columns must include PK but that is often not in Form
               # if is an autoincrem value
                my @fields = @{ $foreign_form->metadata->field_methods };
                push( @fields, ref $foreign_pk ? @$foreign_pk : $foreign_pk );
                $serial_args{relname_fields} = \@fields;
            }
            $c->stash( serial_args => \%serial_args );

            unless ( defined $c->res->status and $c->res->status =~ m/^[45]/ )
            {
                $c->res->content_type( $self->json_mime );

                # in case postcommit() set these
                $c->res->location('');
                $c->res->status(200);
            }
        }

        $c->log->debug("view_class = $view_class") if $c->debug;
        $c->log->debug( "current_view set to " . $c->stash->{current_view} )
            if $c->debug;

    }
}

1;

__END__

=head1 AUTHOR

Peter Karman, C<< <karman@cpan.org> >>

=head1 BUGS

Please report any bugs or feature requests to
C<bug-catalystx-crud-yui@rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org>.  I will be notified, and then you'll automatically be
notified of progress on your bug as I make changes.

=head1 ACKNOWLEDGEMENTS

The Minnesota Supercomputing Institute C<< http://www.msi.umn.edu/ >>
sponsored the development of this software.

=head1 COPYRIGHT & LICENSE

Copyright 2008 by the Regents of the University of Minnesota.


This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=cut