File Coverage

blib/lib/Rose/HTMLx/Form/Related.pm
Criterion Covered Total %
statement 33 149 22.1
branch 0 74 0.0
condition 0 28 0.0
subroutine 11 29 37.9
pod 14 14 100.0
total 58 294 19.7


line stmt bran cond sub pod time code
1             package Rose::HTMLx::Form::Related;
2              
3 1     1   34943 use warnings;
  1         3  
  1         31  
4 1     1   6 use strict;
  1         2  
  1         37  
5              
6 1     1   6 use base qw( Rose::HTML::Form );
  1         14  
  1         1404  
7 1     1   258645 use Carp;
  1         3  
  1         67  
8              
9 1     1   1162 use Rose::HTMLx::Form::Related::Metadata;
  1         3  
  1         32  
10 1     1   1599 use Rose::HTMLx::Form::Field::Boolean;
  1         35051  
  1         20  
11 1     1   1621 use Rose::HTMLx::Form::Field::Autocomplete;
  1         31512  
  1         16  
12 1     1   1355 use Rose::HTMLx::Form::Field::Serial;
  1         280  
  1         12  
13 1     1   1623 use Rose::HTML::Form::Field::PopUpMenu;
  1         21697  
  1         13  
14 1     1   1212 use Rose::HTMLx::Form::Field::PopUpMenuNumeric;
  1         808  
  1         18  
15              
16             __PACKAGE__->field_type_class(
17             boolean => 'Rose::HTMLx::Form::Field::Boolean' );
18             __PACKAGE__->field_type_class(
19             autocomplete => 'Rose::HTMLx::Form::Field::Autocomplete' );
20             __PACKAGE__->field_type_class( serial => 'Rose::HTMLx::Form::Field::Serial' );
21             __PACKAGE__->field_type_class(
22             nummenu => 'Rose::HTMLx::Form::Field::PopUpMenuNumeric' );
23              
24             use Rose::Object::MakeMethods::Generic (
25 1         13 'scalar --get_set_init' =>
26             [qw( metadata metadata_class app_class debug )],
27              
28 1     1   76 );
  1         3  
29              
30             our $VERSION = '0.24';
31              
32             =head1 NAME
33              
34             Rose::HTMLx::Form::Related - RHTMLO forms, living together
35              
36             =head1 SYNOPSIS
37              
38             package MyForm;
39             use strict;
40             use parent 'Rose::HTMLx::Form::Related';
41            
42             sub init_metadata {
43             my $self = shift;
44             return $self->metadata_class->new(
45             form => $self,
46             object_class => 'MyORMClass',
47             );
48             }
49            
50             1;
51              
52             =head1 DESCRIPTION
53              
54             Rose::HTMLx::Form::Related is a subclass of Rose::HTML::Form.
55             Rose::HTMLx::Form::Related can interrogate the relationships
56             between ORM classes and the Forms that represent them,
57             and use that data to tie multiple Rose::HTMLx::Form::Related
58             classes together.
59              
60             There are some additional convenience methods provided, as well
61             as the addition to two more field types (B and B)
62             not part of the standard Rose::HTML::Form installation.
63              
64             =head1 METHODS
65              
66             =cut
67              
68             =head2 init
69              
70             Overrides base method to call interrelate_fields() if
71             metadata->interrelate_fields() is true.
72              
73             =cut
74              
75             sub init {
76 0     0 1   my $self = shift;
77 0           $self->SUPER::init(@_);
78 0           for my $field ( $self->fields ) {
79 0           $field->xhtml_error_separator(''); # let CSS decide space
80             }
81 0 0         $self->interrelate_fields if $self->metadata->interrelate_fields;
82             }
83              
84             =head2 init_metadata
85              
86             Creates and returns a Rose::HTMLx::Form::Related::Metadata object.
87             This method will not be called if a metadata object is passed in new().
88              
89             =cut
90              
91             sub init_metadata {
92 0     0 1   croak "must define init_metadata() or pass Metadata object in new()";
93             }
94              
95             =head2 init_metadata_class
96              
97             Returns name of the metadata class.
98             Default is 'Rose::HTMLx::Form::Related::Metadata'.
99              
100             =cut
101              
102             sub init_metadata_class {
103 0     0 1   return 'Rose::HTMLx::Form::Related::Metadata';
104             }
105              
106             =head2 init_app_class
107              
108             Returns name of class whose instances are stored in app().
109              
110             Default is the emptry string.
111              
112             =cut
113              
114 0     0 1   sub init_app_class {''}
115              
116             =head2 init_debug
117              
118             Returns 0 by default, unless the RHTMLO_DEBUG env var is set to a true
119             value.
120              
121             =cut
122              
123 0 0   0 1   sub init_debug { $ENV{RHTMLO_DEBUG} || 0 }
124              
125             =head2 object_class
126              
127             Shortcut to metadata->object_class.
128              
129             =cut
130              
131             sub object_class {
132 0     0 1   shift->metadata->object_class;
133             }
134              
135             =head2 hidden_to_text_field( I )
136              
137             Returns a Text field based on I.
138              
139             =cut
140              
141             sub hidden_to_text_field {
142 0     0 1   my $self = shift;
143 0 0         my $hidden = shift or croak "need Hidden Field object";
144 0 0 0       unless ( ref $hidden && $hidden->isa('Rose::HTML::Form::Field::Hidden') )
145             {
146 0           croak "$hidden is not a Rose::HTML::Form::Field::Hidden object";
147             }
148 0           my @attr = ( size => 12 );
149 0           for my $attr (qw( name label class required )) {
150 0           push( @attr, $attr, $hidden->$attr );
151             }
152 0           return Rose::HTML::Form::Field::Text->new(@attr);
153             }
154              
155             =head2 field_names_by_rank
156              
157             Returns array ref of field names sorted numerically by their rank attribute.
158              
159             =cut
160              
161             sub field_names_by_rank {
162 0     0 1   my $self = shift;
163 0           my @new = map { $_->name }
  0            
164 0           sort { $a->rank <=> $b->rank } $self->fields;
165 0           return [@new];
166             }
167              
168             =head2 interrelate_fields( [ I ] )
169              
170             Called by init() after the SUPER::init() method has been called
171             if the metadata->interrelate_fields boolean is true (the default).
172              
173             interrelate_fields() will convert fields that return true from
174             metadata->related_field() to menu or autocomplete
175             type fields based on foreign key metadata from metadata->object_class().
176              
177             In other words, interrelate_fields() will convert your many-to-one
178             foreign-key relationships into HTML fields that help
179             enforce the relationship.
180              
181             The I argument is the maximum number of values to consider before
182             creating an autocomplete field instead of a menu field. The default is
183             50, which is a reasonable number of options in a HTML menu.
184              
185             =cut
186              
187             my %count_cache; # memoize to reduce db trips
188              
189             sub interrelate_fields {
190 0     0 1   my $self = shift;
191 0           my $max = shift;
192 0 0         if ( !defined $max ) {
193 0           $max = 50;
194             }
195              
196 0           for my $field ( @{ $self->metadata->related_field_names } ) {
  0            
197 0 0         my $rel_info = $self->metadata->related_field($field) or next;
198              
199             # do not bother with relationships that represent
200             # multiple columns, since these really need a field type
201             # that does not exist: something like CXC::YUI DataTable picker
202 0 0         if ( scalar keys %{ $rel_info->foreign_column } > 1 ) {
  0            
203 0 0         $self->debug
204             and warn
205             "too many columns in rel_info $rel_info->{name} for $field";
206 0           next;
207             }
208              
209 0   0       my $count = $count_cache{ $rel_info->foreign_class }
210             || $self->get_objects_count(
211             object_class => $rel_info->foreign_class );
212              
213             # defer $count == undef, as with DBIC deploy()
214             # TODO someway to re-try later??
215 0 0         unless ( defined $count ) {
    0          
216 0 0         if ( $self->debug ) {
217 0           warn "get_objects_count returned undef for $field";
218             }
219 0           return;
220             }
221             elsif ( $self->debug ) {
222 0           warn "get_objects_count returned $count for $field (max = $max)";
223             }
224              
225 0           $count_cache{ $rel_info->foreign_class } = $count;
226              
227 0 0         if ( $count > $max ) {
228 0           $self->_convert_field_to_autocomplete( $field, $rel_info );
229             }
230             else {
231 0           $self->_convert_field_to_menu( $field, $rel_info );
232             }
233             }
234              
235 0 0         $self->debug and warn "interrelated fields complete for $self";
236             }
237              
238             =head2 get_objects_count( object_class => I )
239              
240             Returns an integer reflecting the number of objects
241             available for iterrelate_fields().
242              
243             The default is C<100> which is useless. You should override this method
244             in your subclass to actually query the db. All counts are memoized
245             internally so get_objects_count() will only ever be called
246             once per I.
247              
248             =cut
249              
250 0     0 1   sub get_objects_count {100}
251              
252             =head2 get_objects( object_class => I )
253              
254             Returns an array ref of objects of type I. The
255             array ref is used by interrelate_fields() to auto-populate
256             popup menus.
257              
258             The default is to return an empty array ref. Override
259             in your subclass to do something more meaningful.
260              
261             Note that get_objects() will be called by clear() and reset()
262             so that you can cache Form objects and always get up-to-date
263             menu options.
264              
265             =cut
266              
267 0     0 1   sub get_objects { [] }
268              
269             sub __set_menu_options {
270 0     0     my ( $self, $menu, $rel_info ) = @_;
271 0           my $field_name = $menu->name;
272 0           my $fk = $rel_info->foreign_column_for($field_name);
273 0           my $to_show
274             = $self->metadata->show_related_field_using( $rel_info->foreign_class,
275             $field_name );
276              
277 0 0         $self->debug and warn "$field_name $fk -> $to_show";
278              
279 0 0         return if !defined $to_show;
280              
281 0           my $objects
282             = $self->get_objects( object_class => $rel_info->foreign_class );
283 0           my $hash = { map { $_->$fk => $_->$to_show } @$objects };
  0            
284              
285             # allow for a non-value (null)
286             # which is particularly useful for search forms
287 0 0         unless ( exists $hash->{''} ) {
288 0           $hash->{''} = '';
289             }
290              
291             $menu->options(
292 0           [ sort { $hash->{$a} cmp $hash->{$b} }
  0            
293             keys %$hash
294             ]
295             );
296 0           $menu->labels($hash);
297 0           return $menu;
298             }
299              
300             sub _convert_field_to_menu {
301 0     0     my $self = shift;
302 0           my $field_name = shift;
303 0           my $rel_info = shift;
304              
305 0           my $field = $self->field($field_name);
306 0 0         return if $field->isa('Rose::HTML::Form::Field::Hidden');
307 0 0 0       return if defined $field->type and $field->type eq 'hidden';
308 0 0         return if $field->isa('Rose::HTML::Form::Field::PopUpMenu');
309              
310 0 0         $self->debug and warn "$field_name converting to menu";
311              
312 0 0         my $menu_class
313             = $field->isa('Rose::HTML::Form::Field::Numeric')
314             ? 'Rose::HTMLx::Form::Field::PopUpMenuNumeric'
315             : 'Rose::HTML::Form::Field::PopUpMenu';
316              
317 0   0       my $menu = $menu_class->new(
      0        
318             id => $field->id,
319             name => $field_name,
320             type => 'menu',
321             class => 'interrelated ' . ( $field->class || '' ),
322             label => $rel_info->label || $field->label,
323             tabindex => $field->tabindex,
324             rank => $field->rank,
325             );
326              
327 0 0         if ( defined $field->description ) {
328 0           $menu->description( $field->description );
329             }
330 0 0         $self->__set_menu_options( $menu, $rel_info ) or return;
331              
332             # must delete first since field() will return cached $field
333             # if it already has been added.
334 0           $self->delete_field($field);
335 0           $self->field( $field_name => $menu );
336             }
337              
338             sub _convert_field_to_autocomplete {
339 0     0     my $self = shift;
340 0           my $field_name = shift;
341 0           my $rel_info = shift;
342 0           my $field = $self->field($field_name);
343 0 0         return if $field->isa('Rose::HTML::Form::Field::Hidden');
344 0 0 0       return if defined $field->type and $field->type eq 'hidden';
345 0 0         return if $field->isa('Rose::HTMLx::Form::Field::Autocomplete');
346 0 0         return if !$field->isa('Rose::HTML::Form::Field::Text');
347              
348             #dump $self;
349 0 0 0       my $app = $self->app || $self->app_class
350             or croak "app() or app_class() required for autocomplete";
351 0 0         unless ( $app->can('uri_for') ) {
352 0           croak "app $app does not implement a uri_for() method";
353             }
354              
355 0 0         $self->debug && warn "convert $field_name to autocomplete";
356              
357 0           my $to_show
358             = $self->metadata->show_related_field_using( $rel_info->foreign_class,
359             $field_name );
360              
361 0 0         return if !defined $to_show;
362              
363 0 0         $self->debug && warn "show_related field using $to_show";
364              
365 0 0         my $controller = $rel_info->get_controller or return;
366              
367 0   0       my $ac = Rose::HTMLx::Form::Field::Autocomplete->new(
      0        
368             id => $field->id,
369             type => 'autocomplete',
370             class => 'interrelated autocomplete ' . ( $field->class || '' ),
371             label => $rel_info->label || $field->label,
372             tabindex => $field->tabindex,
373             rank => $field->rank,
374             size => 30, # ignore original $field size
375             maxlength => $field->maxlength,
376             autocomplete =>
377             $app->uri_for( '/' . $controller->path_prefix, 'autocomplete' ),
378             limit => 30,
379             );
380              
381 0 0         if ( defined $field->description ) {
382 0           $ac->description( $field->description );
383             }
384              
385             # must delete first since field() will return cached $field
386             # if it already has been added.
387 0           $self->delete_field($field);
388 0           $self->field( $field_name => $ac );
389              
390             }
391              
392             =head2 init_with_object
393              
394             Overrides base method to always return the Form object that called
395             the method.
396              
397             =cut
398              
399             sub init_with_object {
400 0     0 1   my $self = shift;
401 0           my $ret = $self->SUPER::init_with_object(@_);
402 0           return $self;
403             }
404              
405             =head2 clear
406              
407             Overrides base method to reset any options in any interrelated
408             menu fields by calling get_objects() again.
409              
410             =head2 reset
411              
412             Overrides base method to reset any options in any interrelated
413             menu fields by calling get_objects() again.
414              
415             =cut
416              
417             sub clear {
418 0     0 1   my $self = shift;
419 0           $self->SUPER::clear(@_);
420 0           $self->__reset_menu_options;
421 0           return $self;
422             }
423              
424             sub reset {
425 0     0 1   my $self = shift;
426 0           $self->SUPER::reset(@_);
427 0           $self->__reset_menu_options;
428 0           return $self;
429             }
430              
431             sub __reset_menu_options {
432 0     0     my $self = shift;
433 0           for my $field ( @{ $self->fields } ) {
  0            
434 0 0         next unless $field->isa('Rose::HTML::Form::Field::PopUpMenu');
435 0 0 0       next unless $field->class && $field->class =~ m/interrelated/;
436              
437 0 0         my $rel_info = $self->metadata->related_field( $field->name ) or next;
438 0           $self->__set_menu_options( $field, $rel_info );
439             }
440             }
441              
442             1;
443              
444             __END__