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 =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 param will be appended to all search queries via the .tt files. See CatalystX::CRUD::Model::Utils for more documentation about the C param. =item default_view The name of the View to use if C 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 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, I ) 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 ) 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, I ) Lke livegrid_create_form but returns form initialized for record represented by I. 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, I ) 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, I ) Sets up stash() to mimic the foreign controller represented by I. =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: This URI is for ManyToMany only. Using it on OneToMany or ManyToOne I 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,
,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 ) 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 if it is not set, based on the C request parameter, defaulting to 'YUI'. BThis assumes you have a View class called C 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<< >> =head1 BUGS Please report any bugs or feature requests to C, or through the web interface at L. 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