]> git.decadent.org.uk Git - maypole.git/blob - lib/Maypole/Model/CDBI/DFV.pm
another change to object stringification in selects in AsForm
[maypole.git] / lib / Maypole / Model / CDBI / DFV.pm
1 package Maypole::Model::CDBI::DFV;
2 use strict;
3
4 =head1 NAME
5
6 Maypole::Model::CDBI::DFV - Class::DBI::DFV model for Maypole.
7
8 =head1 SYNOPSIS
9
10     package Foo;
11     use 'Maypole::Application';
12
13     Foo->config->model("Maypole::Model::CDBI::DFV");
14     Foo->setup([qw/ Foo::SomeTable Foo::Other::Table /]);
15
16     # Look ma, no untainting
17
18     sub Foo::SomeTable::SomeAction : Exported {
19
20         . . .
21
22     }
23
24 =head1 DESCRIPTION
25
26 This module allows you to use Maypole with previously set-up
27 L<Class::DBI> classes that use Class::DBI::DFV;
28
29 Simply call C<setup> with a list reference of the classes you're going to use,
30 and Maypole will work out the tables and set up the inheritance relationships
31 as normal.
32
33 Better still, it will also set use your DFV profile to validate input instead
34 of CGI::Untaint. For teh win!!
35
36 =cut
37
38 use Class::C3;
39 use Data::FormValidator;
40 use Data::Dumper;
41
42 use Maypole::Config;
43 use Maypole::Model::CDBI::AsForm;
44
45 use base qw(Maypole::Model::CDBI::Base);
46
47 Maypole::Config->mk_accessors(qw(table_to_class _COLUMN_INFO));
48
49 =head1 METHODS
50
51 =head2 setup
52
53   This method is inherited from Maypole::Model::Base and calls setup_database,
54   which uses Class::DBI::Loader to create and load Class::DBI classes from
55   the given database schema.
56
57 =head2 setup_database
58
59   This method loads the model classes for the application
60
61 =cut
62
63 sub setup_database {
64     my ( $self, $config, $namespace, $classes ) = @_;
65     $config->{classes}        = $classes;
66     foreach my $class (@$classes) {
67       $namespace->load_model_subclass($class);
68     }
69     $namespace->model_classes_loaded(1);
70     $config->{table_to_class} = { map { $_->table => $_ } @$classes };
71     $config->{tables}         = [ keys %{ $config->{table_to_class} } ];
72 }
73
74 =head2 class_of
75
76   returns class for given table
77
78 =cut
79
80 sub class_of {
81     my ( $self, $r, $table ) = @_;
82     return $r->config->{table_to_class}->{$table};
83 }
84
85 =head2 adopt
86
87 This class method is passed the name of a model class that represensts a table
88 and allows the master model class to do any set-up required.
89
90 =cut
91
92 sub adopt {
93     my ( $self, $child ) = @_;
94     if ( my $col = $child->stringify_column ) {
95         $child->columns( Stringify => $col );
96     }
97 }
98
99 =head2 check_params
100
101   Checks parameters against the DFV profile for the class, returns the results
102   of DFV's check.
103
104   my $dfv_results = __PACKAGE__->check_params($r->params);
105
106 =cut
107
108 sub check_params {
109   my ($class,$params) = @_;
110   return Data::FormValidator->check($params, $class->dfv_profile);
111 }
112
113
114 =head1 Action Methods
115
116 Action methods are methods that are accessed through web (or other public) interface.
117
118 Inherited from L<Maypole::Model::CDBI::Base> except do_edit (below)
119
120 =head2 do_edit
121
122 If there is an object in C<$r-E<gt>objects>, then it should be edited
123 with the parameters in C<$r-E<gt>params>; otherwise, a new object should
124 be created with those parameters, and put back into C<$r-E<gt>objects>.
125 The template should be changed to C<view>, or C<edit> if there were any
126 errors. A hash of errors will be passed to the template.
127
128 =cut
129
130 sub do_edit : Exported {
131   my ($class, $r, $obj) = @_;
132
133   my $config   = $r->config;
134   my $table    = $r->table;
135
136   # handle cancel button hit
137   if ( $r->params->{cancel} ) {
138     $r->template("list");
139     $r->objects( [$class->retrieve_all] );
140     return;
141   }
142
143   my $errors;
144   if ($obj) {
145     ($obj,$errors) = $class->_do_update($r,$obj);
146   } else {
147     ($obj,$errors) = $class->_do_create($r);
148   }
149
150   # handle errors, if none, proceed to view the newly created/updated object
151   if (ref $errors) {
152     # pass errors to template
153     $r->template_args->{errors} = $errors;
154     foreach my $error (keys %$errors) {
155       $r->template_args->{errors}{ucfirst($error)} = $errors->{$error}
156     }
157
158     # Set it up as it was:
159     $r->template_args->{cgi_params} = $r->params;
160     $r->template("edit");
161   } else {
162     $r->template("view");
163   }
164
165   $r->objects( $obj ? [$obj] : []);
166 }
167
168 sub _do_update {
169   my ($class,$r,$obj) = @_;
170   my $errors;
171   my $dfv_results = Data::FormValidator->check($r->{params}, $class->dfv_profile);
172
173   # handle dfv errors
174   if ( $dfv_results->has_missing ) {   # missing fields
175     foreach my $field ( $dfv_results->missing ) {
176       $errors->{$field} = "$field is required";
177     }
178   }
179   if ( $dfv_results->has_invalid ) {   # Print the name of invalid fields
180     foreach my $field ( $dfv_results->invalid ) {
181       $errors->{$field} =  "$field is invalid: " . $dfv_results->invalid( $field );
182     }
183   }
184
185   my $this_class_params = { map { $_ => $r->{params}{$_} }  $class->columns  };
186
187   # update or make other related (must_have, might_have, has_many  etc )
188   unless ($errors) {
189     foreach my $accssr ( grep ( !(exists $this_class_params->{$_}) , keys %{$r->{params}} ) ) {
190       # get related object if it exists
191       my $rel_meta = $class->related_meta('r',$accssr);
192       if (!$rel_meta) {
193         $class->_carp("No relationship for $accssr in " . ref($class));
194         next;
195       }
196
197       my $rel_type  = $rel_meta->{name};
198       my $fclass    = $rel_meta->{foreign_class};
199       my ($rel_obj,$errs);
200       $rel_obj = $fclass->retrieve($r->params->{$accssr});
201       # update or create related object
202       ($rel_obj, $errs) = ($rel_obj)
203         ? $fclass->_do_update($r, $rel_obj)
204           : $obj->_create_related($accssr, $r->params);
205       $errors->{$accssr} = $errs if ($errs);
206     }
207   }
208
209   unless ($errors) {
210     $obj->set( %$this_class_params );
211     $obj->update;
212   }
213
214   return ($obj,$errors);
215
216 }
217
218 sub _do_create {
219   my ($class,$r) = @_;
220   my $errors;
221   my $this_class_params = { map { $_ => $r->{params}{$_} }  $class->columns  };
222   my $obj;
223
224   my $dfv_results = Data::FormValidator->check($r->{params}, $class->dfv_profile);
225   if ($dfv_results->success) {
226     $obj = $class->create($this_class_params);
227   } else {
228     # handle dfv errors
229     if ( $dfv_results->has_missing ) {   # missing fields
230       foreach my $field ( $dfv_results->missing ) {
231         $errors->{$field} = "$field is required";
232       }
233     }
234     if ( $dfv_results->has_invalid ) {   # Print the name of invalid fields
235       foreach my $field ( $dfv_results->invalid ) {
236         $errors->{$field} =  "$field is invalid: " . $dfv_results->invalid( $field );
237       }
238     }
239   }
240
241   # Make other related (must_have, might_have, has_many  etc )
242   unless ($errors) {
243     foreach my $accssr ( grep ( !(exists $this_class_params->{$_}) , keys %{$r->{params}} ) ) {
244       my ($rel_obj, $errs) = $obj->_create_related($accssr, $r->{params}{$accssr});
245       $errors->{$accssr} = $errs if ($errs);
246     }
247   }
248   return ($obj,$errors);
249 }
250
251
252 sub _create_related {
253   # self is object or class, accssr is accssr to relationship, params are
254   # data for relobject, and created is the array ref to store objs
255   my ( $self, $accssr, $params )  = @_;
256   $self->_croak ("Can't make related object without a parent $self object") unless (ref $self);
257   my $created = [];
258   my $rel_meta = $self->related_meta('r',$accssr);
259   if (!$rel_meta) {
260     $self->_carp("No relationship for $accssr in " . ref($self));
261     return;
262   }
263
264   my $rel_type  = $rel_meta->{name};
265   my $fclass    = $rel_meta->{foreign_class};
266
267   my ($rel, $errs);
268
269   # Set up params for might_have, has_many, etc
270   if ($rel_type ne 'has_own' and $rel_type ne 'has_a') {
271     # Foreign Key meta data not very standardized in CDBI
272     my $fkey= $rel_meta->{args}{foreign_key} || $rel_meta->{foreign_column};
273     unless ($fkey) { die " Could not determine foreign key for $fclass"; }
274     my %data = (%$params, $fkey => $self->id);
275     %data = ( %data, %{$rel_meta->{args}->{constraint} || {}} );
276     ($rel, $errs) =  $fclass->_do_create(\%data);
277   }
278   else {
279     ($rel, $errs) =  $fclass->_do_create($params);
280     unless ($errs) {
281       $self->$accssr($rel->id);
282       $self->update;
283     }
284   }
285   return ($rel, $errs);
286 }
287
288
289 =head2 do_delete
290
291 Inherited from Maypole::Model::CDBI::Base.
292
293 This action deletes records
294
295 =head2 do_search
296
297 Inherited from Maypole::Model::CDBI::Base.
298
299 This action method searches for database records.
300
301 =head2 list
302
303 Inherited from Maypole::Model::CDBI::Base.
304
305 The C<list> method fills C<$r-E<gt>objects> with all of the
306 objects in the class. The results are paged using a pager.
307
308 =cut
309
310 sub _column_info {
311   my $class = shift;
312
313   # get COLUMN INFO from DB
314   $class->SUPER::_column_info() unless (ref $class->COLUMN_INFO);
315
316   # update with required columns from DFV Profile
317   my $profile = $class->dfv_profile;
318   $class->required_columns($profile->{required});
319
320   return $class->COLUMN_INFO;
321 }
322
323
324
325 =head1 SEE ALSO
326
327 L<Maypole::Model::Base>
328
329 L<Maypole::Model::CDBI::Base>
330
331 =head1 AUTHOR
332
333 Aaron Trevena.
334
335 =head1 LICENSE
336
337 You may distribute this code under the same terms as Perl itself.
338
339 =cut
340
341 1;
342
343