File Coverage

blib/lib/HTML/FormHandler/Model/CDBI.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package # hide from Pause
2             HTML::FormHandler::Model::CDBI;
3             # ABSTRACT: Class::DBI model class (non-functioning)
4              
5 1     1   9136 use Moose;
  0            
  0            
6             use Carp;
7             use Data::Dumper;
8             extends 'HTML::FormHandler';
9              
10             our $VERSION = '0.02';
11              
12              
13             HTML::FormHandler::Model::CDBI->meta->make_immutable;
14              
15              
16             sub init_item {
17             my $self = shift;
18              
19             my $item_id = $self->item_id or return;
20             return $self->item_class->retrieve($item_id);
21             }
22              
23             sub BUILDARGS {
24             my ( $self, @args ) = @_;
25             return {@args};
26             }
27              
28              
29             sub guess_field_type {
30             my ( $self, $column, $class ) = @_;
31              
32             $class ||= $self->item_class;
33              
34             return unless $class && $class->isa('Class::DBI');
35              
36             my @return;
37              
38             # Is it a direct has_a relationship?
39             if ( my $meta = $class->meta_info('has_a')->{$column} ) {
40             my $f_class = $meta->foreign_class;
41              
42             @return =
43             $f_class->isa('DateTime') ? ('DateTime') :
44             ( 'Select', $f_class );
45              
46             # Otherwise, check for has_many
47             }
48             elsif ( $meta = $class->meta_info('has_many')->{$column} ) {
49              
50             my $f_class = $meta->foreign_class;
51             # Is there a mapping table in between? If so need to find the
52             # actual class for lookups -- call recursively
53             if ( @{ $meta->args->{mapping} } ) {
54             my $t;
55             ( $t, $f_class ) = $self->guess_field_type( $meta->args->{mapping}[0], $f_class );
56             }
57             @return = ( 'Multiple', $f_class );
58             }
59             elsif ( $column =~ /_time$/ ) {
60             @return = ('DateTime');
61             }
62             else {
63             @return = ('Text');
64             }
65              
66             return wantarray ? @return : $return[0];
67             }
68              
69              
70             sub lookup_options {
71             my ( $self, $field ) = @_;
72              
73             my $class = $self->item_class or return;
74             return unless $class->isa('Class::DBI');
75             my $field_name = $field->name;
76             my ( $type, $f_class ) = $self->guess_field_type( $field_name, $class );
77             return unless $f_class;
78              
79             # label column
80             my $label_column = $field->label_column;
81             return unless $f_class->find_column($label_column);
82             # active column
83             my $active_col =
84             $self->can('active_column') ? $self->active_column :
85             $field->active_column;
86             $active_col = '' unless $f_class->find_column($active_col);
87             # sort column
88             my $sort_col = $field->sort_column;
89             $sort_col =
90             defined $sort_col && $f_class->find_column($sort_col) ? $sort_col :
91             $label_column;
92              
93             my $criteria = {};
94             my $primary_key = $f_class->primary_column;
95             # In cases where the f_class is the same as the item's class don't
96             # include item in the option list -- don't want to be able to have item point to itself
97             # Obviously, this doesn't prevent circular references.
98             $criteria->{"$primary_key"} = { '!=', $self->item->id }
99             if $f_class eq ref $self->item;
100              
101             # If there's an active column, only select active OR items already selected
102             if ($active_col) {
103             my @or = ( $active_col => 1 );
104             # But also include any existing non-active
105             push @or, ( "$primary_key" => $field->init_value ) # init_value is scalar or array ref
106             if $self->item && defined $field->init_value;
107             $criteria->{'-or'} = \@or;
108             }
109              
110             my @rows = $f_class->search( $criteria, { order_by => $sort_col } );
111              
112             return [
113             map {
114             my $label = $_->$label_column;
115             $_->id, $active_col && !$_->$active_col ? "[ $label ]" : "$label"
116             } @rows
117             ];
118              
119             }
120              
121              
122             sub init_value {
123             my ( $self, $field, $item ) = @_;
124              
125             my $column = $field->name;
126              
127             $item ||= $self->item;
128             return if $field->writeonly;
129             return
130             unless $item &&
131             ( $item->can($column) ||
132             ( ref $item eq 'HASH' && exists $item->{$column} ) );
133             my @values;
134             if ( ref $item eq 'HASH' ) {
135             @values = $item->{$column} if ref($item) eq 'HASH';
136             }
137             elsif ( !$item->isa('Class::DBI') ) {
138             @values = $item->$column;
139             }
140             else {
141             @values =
142             map { ref $_ && $_->isa('Class::DBI') ? $_->id : $_ } $item->$column;
143             }
144              
145             my $value = @values > 1 ? \@values : shift @values;
146             $field->init_value($value);
147             $field->value($value);
148             }
149              
150              
151             sub validate_model {
152             my ($self) = @_;
153              
154             return unless $self->validate_unique;
155             return 1;
156             }
157              
158              
159             sub validate_unique {
160             my ($self) = @_;
161              
162             my @unique = map { $_->name } grep { $_->unique } $self->fields;
163             return 1 unless @unique;
164              
165             my $item = $self->item;
166              
167             my $class = ref($item) || $self->item_class;
168             my $found_error = 0;
169             for my $field ( map { $self->field($_) } @unique ) {
170             next if $field->errors;
171             my $value = $field->value;
172             next unless defined $value;
173             my $name = $field->name;
174             # unique means there can only be on in the database like it.
175             my $match = $class->search( { $name => $value } )->first || next;
176             next if $self->items_same( $item, $match );
177             my $field_error = $field->unique_message ||
178             'Value must be unique in the database';
179             $field->add_error($field_error);
180             $found_error++;
181             }
182             return $found_error;
183             }
184              
185             sub update_model {
186             my ($self) = @_;
187              
188             # Grab either the item or the object class.
189             my $item = $self->item;
190             my $class = ref($item) || $self->item_class;
191             my $updated_or_created;
192              
193             # get a hash of all fields
194             my %fields = map { $_->name, $_ } grep { !$_->noupdate } $self->fields;
195             # First process the normal and has_a columns
196             # as that data is directly stored in the object
197             my %data;
198             # Loads columns (including has_a)
199             foreach my $col ( $class->columns('All') ) {
200             next unless exists $fields{$col};
201             my $field = delete $fields{$col};
202             # If the field is flagged "clear" then set to NULL.
203             my $value = $field->value;
204             if ($item) {
205             my $cur = $item->$col;
206             next unless $value || $cur;
207             next if $value && $cur && $value eq $cur;
208             $item->$col($value);
209             }
210             else {
211             $data{$col} = $value;
212             }
213             }
214              
215             if ($item) {
216             $item->update;
217             $updated_or_created = 'updated';
218             }
219             else {
220             $item = $class->create( \%data );
221             $self->item($item);
222             $updated_or_created = 'created';
223             }
224              
225             # Now check for mapping/has_many in any left over fields
226              
227             for my $field_name ( keys %fields ) {
228             next unless $class->meta_info('has_many');
229             next unless my $meta = $class->meta_info('has_many')->{$field_name};
230              
231             my $field = delete $fields{$field_name};
232             my $value = $field->value;
233              
234             # Figure out which values to keep and which to add
235             my %keep;
236             %keep = map { $_ => 1 } ref $value ? @$value : ($value)
237             if defined $value;
238              
239             # Get foreign class and its key that points to $class
240             my $foreign_class = $meta->foreign_class;
241             my $foreign_key = $meta->args->{foreign_key};
242             my $related_key = $meta->args->{mapping}->[0];
243             die "Failed to find related_key for field [$field] in class [$class]"
244             unless $related_key;
245              
246             # Delete any items that are not to be kept
247             for ( $foreign_class->search( { $foreign_key => $item } ) ) {
248             $_->delete unless delete $keep{ $_->$related_key };
249             }
250              
251             # Add in new ones
252             $foreign_class->create(
253             {
254             $foreign_key => $item,
255             $related_key => $_,
256             }
257             ) for keys %keep;
258             }
259              
260             # Save item in form object
261             $self->item($item);
262             return $item;
263             }
264              
265              
266             sub items_same {
267             my ( $self, $item1, $item2 ) = @_;
268              
269             # returns true if both are undefined
270             return 1 if not defined $item1 and not defined $item2;
271             # return false if either undefined
272             return unless defined $item1 and defined $item2;
273             return $self->obj_key($item1) eq $self->obj_key($item2);
274             }
275              
276              
277             sub obj_key {
278             my ( $self, $item ) = @_;
279             return join '|', $item->table,
280             map { $_ . '=' . ( $item->$_ || '.' ) } $item->primary_columns;
281             }
282              
283             __PACKAGE__->meta->make_immutable;
284             no Moose;
285             1;
286              
287             __END__
288              
289             =pod
290              
291             =encoding UTF-8
292              
293             =head1 NAME
294              
295             HTML::FormHandler::Model::CDBI - Class::DBI model class (non-functioning)
296              
297             =head1 VERSION
298              
299             version 0.40057
300              
301             =head1 SYNOPSIS
302              
303             package MyApplication::Form::User;
304             use strict;
305             use base 'HTML::FormHandler::Model::CDBI';
306              
307              
308             # Associate this form with a CDBI class
309             has '+item_class' => ( default => 'MyDB::User' );
310              
311             # Define the fields that this form will operate on
312             sub field_list {
313             return {
314             [
315             name => 'Text',
316             age => 'PosInteger',
317             sex => 'Select',
318             birthdate => 'DateTimeDMYHM',
319             ]
320             };
321             }
322              
323             =head1 DESCRIPTION
324              
325             A Class::DBI database model for HTML::FormHandler
326              
327             I don't use CDBI, so this module almost certainly doesn't work.
328             It is only being left here as a starting point in case somebody is
329             interested in getting it to work.
330              
331             Patches and tests gratefully accepted.
332              
333             =head1 METHODS
334              
335             =head2 item_class
336              
337             The name of your database class.
338              
339             =head2 init_item
340              
341             This is called first time $form->item is called.
342             It does the equivalent of:
343              
344             return $self->item_class->retrieve( $self->item_id );
345              
346             =head2 guess_field_type
347              
348             Pass in a column and assigns field types.
349             Must set $self->item_class to return the related item class.
350             Returns the type in scalar context, returns the type and maybe the related table
351             in list context.
352              
353             Currently returns:
354              
355             DateTime - for a has_a relationship that isa DateTime
356             Select - for a has_a relationship
357             Multiple - for a has_many
358             DateTime - if the field ends in _time
359             Text - otherwise
360              
361             =head2 lookup_options
362              
363             Returns a array reference of key/value pairs for the column passed in.
364             Calls $field->label_column to get the column name to use as the label.
365             The default is "name". The labels are sorted by Perl's cmp sort.
366              
367             If there is an "active" column then only active are included, with the exception
368             being if the form (item) has currently selected the inactive item. This allows
369             existing records that reference inactive items to still have those as valid select
370             options. The inactive labels are formatted with brackets to indicate in the select
371             list that they are inactive.
372              
373             The active column name is determined by calling:
374              
375             $active_col = $form->can( 'active_column' )
376             ? $form->active_column
377             : $field->active_column;
378              
379             Which allows setting the name of the active column globally if
380             your tables are consistently named (all lookup tables have the same
381             column name to indicate they are active), or on a per-field basis.
382              
383             In addition, if the foreign class is the same as the item's class (or the class returned
384             by item_class) then options pointing to item are excluded. The reason for this is
385             for a table column that points to the same table (self referenced), such as a "parent"
386             column. The assumption is that a record cannot be its own parent.
387              
388             =head2 init_value
389              
390             Populate $field->value with object ids from the CDBI object. If the column
391             expands to more than one object then an array ref is set.
392              
393             =head2 validate_model
394              
395             Validates fields that are dependent on the model.
396             Currently, "unique" fields are checked to make sure they are unique.
397              
398             This validation happens after other form validation. The form already has any
399             field values entered in $field->value at this point.
400              
401             =head2 validate_unique
402              
403             Checks that the value for the field is not currently in the database.
404              
405             =head2 items_same
406              
407             Returns true if the two passed in cdbi objects are the same object.
408             If both are undefined returns true.
409              
410             =head2 obj_key
411              
412             returns a key for a given object, or undef if the object is undefined.
413              
414             =head1 AUTHOR
415              
416             FormHandler Contributors - see HTML::FormHandler
417              
418             =head1 COPYRIGHT AND LICENSE
419              
420             This software is copyright (c) 2014 by Gerda Shank.
421              
422             This is free software; you can redistribute it and/or modify it under
423             the same terms as the Perl 5 programming language system itself.
424              
425             =cut