# @(#)$Id: StashHelper.pm 1139 2012-03-28 23:49:18Z pjf $ package CatalystX::Usul::Plugin::Model::StashHelper; use strict; use warnings; use version; our $VERSION = qv( sprintf '0.5.%d', q$Rev: 1139 $ =~ /\d+/gmx ); use CatalystX::Usul::Constants; use CatalystX::Usul::Functions qw(exception is_arrayref is_hashref throw); use Class::Null; use Data::Pageset; use Scalar::Util qw(blessed); use TryCatch; # Core stash helper methods sub stash_content { # Push/unshift the content onto the item list for the given stash id my ($self, $content, $id, $clear, $stack_dirn) = @_; $content or return; $id ||= q(sdata); $clear ||= q(clear_form); my $s = $self->context->stash; unless (defined $s->{ $id }) { try { $self->$clear() } catch {} } my $count = @{ $s->{ $id }->{items} || [] }; my $item = { content => $content, id => $id.$count }; if ($stack_dirn) { unshift @{ $s->{ $id }->{items} }, $item } else { push @{ $s->{ $id }->{items} }, $item } $s->{ $id }->{count} = $count + 1; return; } sub stash_meta { # Set attribute value pairs for the given stash id my ($self, $content, $id, $clear) = @_; $content or return; $id ||= q(sdata); $clear ||= q(clear_form); my $s = $self->context->stash; unless (defined $s->{ $id }) { try { $self->$clear() } catch {} } while (my ($attr, $value) = each %{ $content }) { $attr eq q(items) or $s->{ $id }->{ $attr } = $value; } return; } # Stash content methods sub add_field { # Add a field widget definition to the inner frame div my ($self, $content, @rest) = @_; is_hashref $content or return; my $s = $self->context->stash; $content->{widget} = TRUE; # TODO: yuck yuck yuck # If error then resultDisp is too wide coz content haz no padding if (exists $content->{subtype} and $content->{subtype} eq q(html)) { $s->{content}->{class} = q(subtype_html); } elsif (not exists $s->{content}->{class}) { $s->{content}->{class} = q(subtype_normal); } return $self->stash_content( $content, @rest ); } sub add_result { # Add some content the the result div my ($self, $content) = @_; my $s = $self->context->stash; $content or return; chomp $content; $s->{result} and $s->{result}->{items}->[ 0 ] and $s->{result}->{items}->[ -1 ]->{content} .= "\n"; return $self->stash_content( $content, qw(result clear_result) ); } sub form_wrapper { # Wrap a group of form fields with a form element my ($self, $args) = @_; my $s = $self->context->stash; my $nitems = __get_field_count( $s->{sdata}, $args->{nitems} ) or return; my $attrs = { action => $args->{action}, enctype => q(application/x-www-form-urlencoded), method => q(post), name => $args->{name} }; return $self->stash_content( { attrs => $attrs, form => TRUE, nitems => $nitems } ); } sub group_fields { # Enclose a group of form fields in a field set definition my ($self, $args) = @_; my $s = $self->context->stash; my $nitems = __get_field_count( $s->{sdata}, $args->{nitems} ) or return; my $class = exists $args->{class} ? $args->{class} : undef; my $text = $args->{text} || $self->loc( $args->{id} || q(duh) ); return $self->stash_content( { frame_class => $class, group => TRUE, nitems => $nitems, text => $text } ); } # Add field helper methods sub add_append { # Add a widget definition to the append div my ($self, $content) = @_; return $self->add_field( $content, qw(append clear_append) ); } sub add_button { # Add a button widget definition to the button bar div my ($self, $args) = @_; is_hashref $args or return; my $s = $self->context->stash; my $label = $args->{label} || 'Unknown'; my $id = $s->{form}->{name}.q(.).(lc $label); my $button = $s->{buttons}->{ $id } || {}; my $help = $args->{help } || $button->{help }; my $prompt = $args->{prompt} || $button->{prompt}; my $class = $args->{class } || $button->{class } || NUL; my $type = $args->{type } || $button->{type } || q(image); my $content = { id => $id.q(_button), type => q(button), }; my $file; if ($type eq q(vertical)) { $content->{class } = $class || q(vertical markup_button submit); $content->{config} = { args => "[ '${label}' ]", method => "'submitForm'" }; $content->{src } = { content => uc $label }; } else { $class and $content->{class} = $class; $content->{name} = $label } if ($type eq q(image) and $file = $self->_get_image_file( $s, $label )) { $content->{alt} = $label; $content->{src} = $s->{assets}.$file; } $help and $content->{tip } = ($args->{title} || DOTS).TTS.$help; $prompt and $content->{config} = { args => "[ '${label}', '${prompt}' ]", method => "'confirmSubmit'" }; return $self->add_field( $content, qw(button clear_buttons) ); } sub add_buttons { my ($self, @labels) = @_; my $title = $self->loc( 'Action' ); for my $label (@labels) { $self->add_button( { label => $label, title => $title } ); } return; } sub add_chooser { my ($self, $args) = @_; my $s = $self->context->stash; my $attr = $args->{attr } or return; my $field = $args->{field } or return; my $form = $args->{form } or return; my $method = $args->{method } or return; my $val = $args->{value }; my $w_fld = $args->{where_fld}; my $param = {}; $s->{is_popup} = q(true); # Stop JS from caching window size $s->{header }->{title} = $self->loc( 'Select Item' ); $w_fld and $param->{ $w_fld } = $args->{where_val}; $param->{ $field } = { like => $val ? $val : q(%) }; my @items = $self->$method( $param ); $items[ 0 ] or return $self->add_field( { text => $self->loc( 'Nothing selected' ), type => q(label) } ); my $class = ($args->{class} || q(anchor_button fade)).q( submit); my $count = 0; for my $item (@items) { my $text = $item->$attr(); $self->add_field( { class => $class, config => { args => "[ '${form}', '${field}', '${text}' ]", method => q("returnValue") }, frame_class => $args->{frame_class} || q(chooser), href => '#top', id => $field.q(_).$attr.$count++, text => $text, tip => $self->loc( 'Click to select' ), type => q(anchor) } ); } return; } sub add_error { # Handle error thrown by a call to the model my ($self, $e) = @_; my $s = $self->context->stash; my $class = blessed $e; ($class and $e->isa( EXCEPTION_CLASS )) or $e = exception $e; $s->{stacktrace} = $s->{debug} ? (blessed $e)."\n".$e->stacktrace : NUL; return $self->_log_and_stash_error( $e ); } sub add_error_msg { my ($self, $error, @rest) = @_; my $key = (split m{ [\n] }mx, $error)[ 0 ]; my $args = (is_arrayref $rest[ 0 ]) ? $rest[ 0 ] : [ @rest ]; my $e = exception 'error' => $key, 'args' => $args; return $self->_log_and_stash_error( $e ); } sub add_footer { my $self = shift; my $s = $self->context->stash; $self->add_field( $self->_hash_for_footer_line, qw(footer clear_footer) ); $self->add_field( $self->_hash_for_async_footer, qw(footer) ); return $self->stash_meta( { state => $s->{fstate} }, qw(footer) ); } sub add_header { my $self = shift; my $s = $self->context->stash; $self->add_field( $self->_hash_for_logo_link, qw(header clear_header) ); $self->add_field( $self->_hash_for_company_link, qw(header) ); return $self->stash_meta( { title => $s->{title} }, qw(header) ); } sub add_hidden { # Add a hidden input field to the form my ($self, $name, $values) = @_; ($name and defined $values) or return; is_arrayref $values or $values = [ $values ]; for my $value (@{ $values }) { my $content = { default => $value, name => $name, type => q(hidden) }; $self->add_field( $content, qw(hidden clear_hidden) ); } return; } sub add_result_msg { my ($self, @rest) = @_; return $self->add_result( $self->loc( @rest ) ); } sub add_search_hit { return; # You want to override in your subclass } sub add_search_links { my ($self, $page_info, $attrs) = @_; my ($args, $key, $name, $page); $attrs ||= {}; my $s = $self->context->stash; my $hits_per = $attrs->{hits_per}; my $href = $attrs->{href}; my $anchor_class = $attrs->{anchor_class} || q(search fade); my $clear = TRUE; for $page (qw(first_page previous_page pages_in_set next_page last_page)) { if ($page eq q(pages_in_set)) { for (@{ $page_info->pages_in_set }) { if ($_ == $page_info->current_page) { $args = { container => FALSE, text => q(…), type => q(label) }; } else { $args = { class => $anchor_class, container_class => q(label_text), href => $href.$hits_per.SEP.$_, name => q(page).$_, pwidth => 0, text => $_, type => q(anchor) }; } $self->add_field( $args ); } } elsif ($key = $page_info->$page) { $name = (split m{ _ }mx, $page)[ 0 ]; $args = { class => $anchor_class, container_class => q(label_text), href => $href.$hits_per.SEP.$key, name => $name, pwidth => 0, text => $self->loc( $page.q(_anchor) ), type => q(anchor) }; if ($clear) { $args->{frame_class } = q(clearLeft); $args->{prompt } = $self->loc( q(page_prompt) ); $args->{stepno } = 0; } $self->add_field( $args ); $clear = FALSE; } } return; } sub add_sidebar_panel { # Add an Ajax call to the sidebar accordion widget my ($self, $args) = @_; my ($content, $count); my $name = $args->{name}; my $s = $self->context->stash; my $sidebar = $s->{sidebar} || {}; unless ($count = $sidebar->{count} || 0) { $self->_clear_by_id( q(sidebar) ); $s->{sidebar}->{tip} = $self->loc( q(sidebarTip) ); } if ($name eq q(default)) { $content = { class => q(accordion_content heading), container_id => q(glassPanel), header => { class => q(accordion_header), id => $name.q(Header), text => $self->loc( q(sidebarBlankHeader) ) }, id => $name, panel => { class => q(accordion_panel), id => q(panel).$count.q(Content) }, text => $self->loc( q(sidebarBlankContent) ), type => q(sidebarPanel) }; } else { my $action = $name.($args->{action} ? SEP.$args->{action} : NUL); $content = { config => { action => '"'.$action.'"', name => '"'.$name.'"' }, container_id => $name.q(Panel), header => { class => q(accordion_header), id => $name.q(Header), text => $args->{heading} || ucfirst $name }, id => $name, panel => { class => q(accordion_panel), id => q(panel).$count.q(Content) }, text => SPC, type => q(sidebarPanel) }; } $args->{on_complete} and $content->{config}->{onComplete} = $args->{on_complete}; $args->{value} and $content->{config}->{value} = '"'.$args->{value}.'"'; $self->add_field( $content, qw(sidebar clear_sidebar), $args->{unshift} ); return $args->{unshift} ? 0 : $s->{sidebar}->{count} - 1; } sub search_for { throw 'Method search_for not overridden in subclass'; return; } sub search_page { my ($self, $args) = @_; my ($hits, @hits); my $field = $args->{search_field}; my $query = $args->{query }; my $hits_per = $args->{hits_per }; my $offset = $args->{offset }; my $heading = $self->loc( $args->{key}, $query ); my $s = $self->context->stash; my $form = $s->{form}->{name}; my $href = $s->{form}->{action}.SEP.$query.SEP; try { $hits = $self->search_for( { hits_per => $hits_per, page => $offset, query => $query, search_field => $field } ); } catch ($e) { return $self->add_error( $e ) } my $page_info = Data::Pageset->new( { current_page => $offset + 1, entries_per_page => $hits_per, mode => q(slide), total_entries => $hits->total_hits } ); my $link_num = 1 + $hits_per * $offset; my $sub_heading = $self->loc( q(search_results), $offset + 1, $page_info->last_page, scalar @{ $hits->list || [] }, $hits->total_hits || 0 ); $self->clear_form( { class => q(narrow left), heading => { class => q(narrow left), content => $heading, }, sub_heading => { class => q(narrow left), content => $sub_heading, level => 4 } } ); $self->add_search_links( $page_info, { href => $href, hits_per => $hits_per } ); try { for my $hit (@{ $hits->list || [] }) { $self->add_search_hit( $hit, $link_num++, $field ); } } catch ($e) { return $self->add_error( $e ) } $self->add_search_links( $page_info, { href => $href, hits_per => $hits_per } ); return; } # Supporting cast sub check_field_wrapper { # Process Ajax calls to validate form field values my $self = shift; my $id = $self->query_value( q(id) ); my $val = $self->query_value( q(val) ); my $msg; $self->stash_meta( { id => $id.q(_ajax), result => NUL } ); try { $self->check_field( $id, $val ) } catch ($e) { $self->stash_meta( { class_name => q(error) } ); $self->stash_content( $msg = $self->loc( $e->error, $id, $val ) ); $self->context->stash->{debug} and $self->log_debug( $msg ); } return; } sub clear_controls { # Clear contents of multiple divs my $self = shift; $self->clear_footer; $self->clear_menus; $self->clear_quick_links; $self->clear_sidebar; return; } sub get_para_col_class { my ($self, $columns) = @_; $columns ||= 1; my @col_names = ( qw(zero one two three four five six seven eight nine ten eleven twelve thirteen fourteen fifteen) ); my $col_class = $columns > 1 ? $col_names[ $columns ].' multi' : 'one'; $col_class .= 'Column'; return $col_class; } sub stash_para_col_class { my ($self, $key, $n_cols) = @_; my $s = $self->context->stash; return $s->{ $key } = $self->get_para_col_class( $n_cols || 2 ); } sub update_group_membership { my ($self, $args) = @_; my $count = 0; my $method_args = $args->{method_args}; $method_args->{items} = $self->query_array( $args->{field}.q(_added) ); defined $method_args->{items}->[ 0 ] and $count += $args->{add_method}->( $method_args ); $method_args->{items} = $self->query_array( $args->{field}.q(_deleted) ); defined $method_args->{items}->[ 0 ] and $count += $args->{delete_method}->( $method_args ); $count < 1 and throw 'Updated nothing'; return TRUE; } # Clear content methods. Called by the stash content methods on first use sub clear_append { my ($self, $args) = @_; return $self->_clear_by_id( q(append), $args ); } sub clear_buttons { my ($self, $args) = @_; return $self->_clear_by_id( q(button), $args ); } sub clear_footer { my ($self, $args) = @_; return $self->_clear_by_id( q(footer), $args ); } sub clear_form { # Clear the stash of all form content my ($self, $args) = @_; my $s = $self->context->stash; my $id = q(sdata); exists $s->{ $id } and not $args->{force} and return; $self->_clear_by_id( $id, $args ); exists $args->{title} and $s->{title} = $s->{header}->{title} = $args->{title}; $s->{firstfld} = $args->{firstfld} || NUL; return; } sub clear_header { my $self = shift; return $self->_clear_by_id( q(header) ); } sub clear_hidden { my ($self, $args) = @_; return $self->_clear_by_id( q(hidden), $args ); } sub clear_menus { my $self = shift; return $self->_clear_by_id( q(menus) ); } sub clear_quick_links { my $self = shift; return $self->_clear_by_id( q(quick_links) ); } sub clear_result { my ($self, $args) = @_; $self->_clear_by_id( q(result), $args ); $self->context->stash->{result}->{text} = $self->loc( 'Results' ); return; } sub clear_sidebar { my $self = shift; $self->context->stash( sidebar => FALSE ); return; } # Private methods sub _clear_by_id { my ($self, $id, $args) = @_; $id or return; $args ||= {}; my $sid = $self->context->stash->{ $id } ||= {}; my $heading = $args->{heading} ? ( (is_hashref $args->{heading}) ? $args->{heading} : { class => q(banner), content => $args->{heading} } ) : FALSE; $heading and $sid->{heading } = $heading; exists $args->{class } and $sid->{class } = $args->{class }; exists $args->{sub_heading} and $sid->{sub_heading} = $args->{sub_heading}; $sid->{count} = 0; $sid->{items} = []; $sid->{mark } = -1; return; } { my %cache; sub _get_image_file { my ($self, $s, $prefix) = @_; my $dir = $self->catdir( $s->{skindir}, $s->{skin} ); 0 > index $prefix, SPC or $prefix =~ s{ \s+ }{_}gmx; for my $file (map { $prefix.$_ } qw(.png .gif)) { my $path = $self->catfile( $dir, $file ); not exists $cache{ $path } and $cache{ $path } = -f $path; $cache{ $path } and return $file; } return; } } sub _hash_for_async_footer { my $self = shift; my $c = $self->context; my $s = $c->stash; my $id = q(footer.data); my $action = $c->action->reverse; my $function = 'function() { this.rebuild() }'; my $args = "[ 'footer', '${id}', '${action}', ${function} ]"; return { config => [ { 'tools0item1' => { args => $args, method => "'request'" } }, { 'footer.data' => { args => $args, event => "'load'", method => "'requestIfVisible'" } }, ], id => $id, text => $s->{nbsp}, type => q(async) }; } sub _hash_for_company_link { my $self = shift; my $c = $self->context; my $s = $c->stash; my $href = $c->uri_for_action( SEP.q(company) ); return { class => q(header_link fade windows), config => { args => "[ '${href}', { name: 'company' } ]", method => "'openWindow'" }, container_id => q(headerSubTitle), container_class => q(none), href => '#top', id => q(company_link), sep => NUL, text => $s->{company}, tip => DOTS.TTS.$self->loc( q(aboutCompanyTip) ), type => q(anchor) }; } sub _hash_for_footer_line { # Cut on the dotted line toggle the footer visibilty my $self = shift; my $id = q(tools0item1); my $text = $self->loc( q(footerOffText) ); my $alt = $self->loc( q(footerOnText) ); return { class => q(cut_here), config => { args => "[ '${id}', 'footer', '${text}', '${alt}' ]", method => "'toggleSwapText'" }, href => '#top', id => q(footer_line), imgclass => q(scissors_icon), text => NUL, tip => DOTS.TTS.$self->loc( q(footerToggleTip) ), type => q(rule) }; } sub _hash_for_logo_link { my $self = shift; my $s = $self->context->stash; my $href = $s->{server_home} || q(http://).$s->{domain}; return { class => q(logo), container_id => q(companyLogo), fhelp => q(Company Logo), hint_title => $href, href => $href, imgclass => q(logo), sep => NUL, text => $s->{assets}.($s->{logo} || q(logo.png)), tip => $self->loc( q(logoTip) ), type => q(anchor) }; } sub _log_and_stash_error { my ($self, $e) = @_; my $s = $self->context->stash; $s->{leader} = blessed $self; $self->log_error_message( $e, $s ); return $self->add_result( $self->loc( $e->error, $e->args ) ); } # Private functions sub __get_field_count { my ($sid, $nitems) = @_; $nitems ||= $sid->{count} - $sid->{mark} - 1; (not $nitems or $nitems <= 0) and return FALSE; $sid->{mark} = $sid->{count}; return $nitems; } 1; __END__ =pod =head1 Name CatalystX::Usul::Plugin::Model::StashHelper - Convenience methods for stuffing the stash =head1 Version 0.5.$Revision: 1139 $ =head1 Synopsis package CatalystX::Usul; use parent qw(CatalystX::Usul::Base CatalystX::Usul::File); package CatalystX::Usul::Model; use parent qw(Catalyst::Model CatalystX::Usul); package YourApp::Model::YourModel; use parent qw(CatalystX::Usul::Model); =head1 Description Many convenience methods for stuffing/resetting the stash. The form widget definitions will be replaced later by the form building method which is called from the HTML view =head1 Subroutines/Methods =head2 add_append Stuff some content into the stash so that it will appear in the I<append> div in the template. The content is a hash ref which will be interpreted as a widget definition by the form builder which is invoked by the HTML view. Multiple calls push the content onto a stack which is rendered in the order in which it was stacked =head2 add_button Add a button definition to the stash. The template will render these as image buttons on the I<button> div =head2 add_buttons Loop around L</add_button> =head2 add_chooser Generates the data for the popup chooser window which allows a data value to be selected from a list produced by some query. It is intended as a replacement for a popup menu widget where the list of values would be prohibitively long =head2 add_error Stringifies the passed error object, localises the text, logs it as an error and calls L</add_result> to display it at the top of the I<sdata> div =head2 add_error_msg Localises the message text, creates a new error object and calls L</add_error> =head2 add_field Create a widget definition for a form field =head2 add_footer Adds data for a horizontal rule to separate the footer from the rest of the content =head2 add_header Stuffs the stash with the data for the page header =head2 add_hidden Adds a I<hidden> field to the form =head2 add_result Adds the result of forwarding to an an action. This is the I<result> div in the template =head2 add_result_msg Localises the message text and calls L</add_result> =head2 add_search_hit Placeholder should have been implemented in the class that applies this role =head2 add_search_links Adds the sequence of links used in search page results; first page, previous page, list of pages around the current one, next page, and last page =head2 add_sidebar_panel Stuffs the stash with the data necessary to create a panel in the accordion widget on the sidebar =head2 check_field_wrapper $model->check_field_wrapper; Extract parameters from the query and call C<check_field>. Stash the result =head2 clear_append Clears the stash of the widget data used by the region appended to the main data store =head2 clear_buttons Clears button data from the stash =head2 clear_controls Groups the methods that clear the stash of data not used in a minority of pages =head2 clear_footer Clears all footer data. Called by L</add_footer> =head2 clear_form Initialises the I<sdata> div contents. Called by C</stash_content> on first use =head2 clear_header Clears the header data from the form =head2 clear_hidden Clears the hidden fields from the form =head2 clear_menus Clears the stash of the main navigation and tools menu data =head2 clear_quick_links Clears the stash of the quick links navigation data =head2 clear_result Clears the stash of messages from the output of actions =head2 clear_sidebar Clears the stash of the data used by the sidebar accordion widget =head2 form_wrapper Stashes the data used by L<HTML::FormWidgets> to throw I<form> around a group of fields =head2 get_para_col_class $column_class = $model_obj->get_para_col_class( $n_columns ); Converts an integer number into a string representation =head2 group_fields Stashes the data used by L<HTML::FormWidgets> to throw a I<fieldset> around a group of fields =head2 search_for Placeholder returns an instance of L<Class::Null>. Should have been implemented in the interface model subclass =head2 search_page Create a L<KinoSearch> results page =head2 stash_content Pushes the content (usually a widget definition) onto the specified stack. Defaults the I<sdata> stack =head2 stash_meta Adds some meta data to the response for an Ajax call =head2 stash_para_col_class $column_class = $model_obj->stash_para_col_class( $key, $n_columns ); Calls and returns the value from L</get_para_col_class>. Also stashes the value in the C<$key> attribute =head2 update_group_membership $bool = $model_obj->update_group_membership( $args ); Adds/removes lists of attributes from groups =head2 _hash_for_logo_link Returns a content hash ref that renders as a clickable image anchor. The link returns to the web servers default page =head2 _hash_for_footer_line Adds a horizontal rule to separate the footer. Called by L</add_footer> =head1 Configuration and Environment None =head1 Diagnostics None =head1 Dependencies =over 3 =item L<CatalystX::Usul> =item L<Data::Pageset> =item L<Lingua::Flags> =item L<Time::Elapsed> =back =head1 Incompatibilities There are no known incompatibilities in this module. =head1 Bugs and Limitations There are no known bugs in this module. Please report problems to the address below. Patches are welcome =head1 Author Peter Flanigan, C<< <Support at RoxSoft.co.uk> >> =head1 License and Copyright Copyright (c) 2008 Peter Flanigan. All rights reserved This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See L<perlartistic> This program is distributed in the hope that it will be useful, but WITHOUT WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE =cut # Local Variables: # mode: perl # tab-width: 3 # End: