File Coverage

blib/lib/Maypole/Model/CDBI/Base.pm
Criterion Covered Total %
statement 6 6 100.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 8 8 100.0


line stmt bran cond sub pod time code
1             package Maypole::Model::CDBI::Base;
2 1     1   1628 use strict;
  1         3  
  1         53  
3              
4             =head1 NAME
5              
6             Maypole::Model::CDBI::Base - Model base class based on Class::DBI
7              
8             =head1 DESCRIPTION
9              
10             This is a master model class which uses L to do all the hard
11             work of fetching rows and representing them as objects. It is a good
12             model to copy if you're replacing it with other database abstraction
13             modules.
14              
15             It implements a base set of methods required for a Maypole Data Model.
16              
17             It inherits accessor and helper methods from L.
18              
19             =cut
20              
21 1     1   6 use base qw(Maypole::Model::Base Class::DBI);
  1         1  
  1         850  
22             use Class::DBI::AbstractSearch;
23             use Class::DBI::Plugin::RetrieveAll;
24             use Class::DBI::Pager;
25             use Lingua::EN::Inflect::Number qw(to_PL);
26             use attributes ();
27             use Data::Dumper;
28              
29             __PACKAGE__->mk_classdata($_) for (qw/COLUMN_INFO/);
30              
31             =head2 add_model_superclass
32              
33             Adds model as superclass to model classes (if necessary)
34              
35             =cut
36              
37             sub add_model_superclass {
38             my ($class,$config) = @_;
39             foreach my $subclass ( @{ $config->classes } ) {
40             next if $subclass->isa("Maypole::Model::Base");
41             no strict 'refs';
42             push @{ $subclass . "::ISA" }, $config->model;
43             }
44             return;
45             }
46              
47             =head1 Action Methods
48              
49             Action methods are methods that are accessed through web (or other public) interface.
50              
51             =head2 do_edit
52              
53             If there is an object in C<$r-Eobjects>, then it should be edited
54             with the parameters in C<$r-Eparams>; otherwise, a new object should
55             be created with those parameters, and put back into C<$r-Eobjects>.
56             The template should be changed to C, or C if there were any
57             errors. A hash of errors will be passed to the template.
58              
59             =cut
60              
61             sub do_edit : Exported {
62             my ($self, $r, $obj) = @_;
63              
64             my $config = $r->config;
65             my $table = $r->table;
66              
67             # handle cancel button hit
68             if ( $r->{params}->{cancel} ) {
69             $r->template("list");
70             $r->objects( [$self->retrieve_all] );
71             return;
72             }
73              
74             my $required_cols = $config->{$table}{required_cols} || $self->required_columns;
75             my $ignored_cols = $config->{$table}{ignore_cols} || [];
76              
77             ($obj, my $fatal, my $creating) = $self->_do_update_or_create($r, $obj, $required_cols, $ignored_cols);
78              
79             # handle errors, if none, proceed to view the newly created/updated object
80             my %errors = $fatal ? (FATAL => $fatal) : $obj->cgi_update_errors;
81              
82             if (%errors) {
83             # Set it up as it was:
84             $r->template_args->{cgi_params} = $r->params;
85              
86             # replace user unfriendly error messages with something nicer
87              
88             foreach (@{$config->{$table}->{required_cols}}) {
89             next unless ($errors{$_});
90             my $key = $_;
91             s/_/ /g;
92             $r->template_args->{errors}{ucfirst($_)} = 'This field is required, please provide a valid value';
93             $r->template_args->{errors}{$key} = 'This field is required, please provide a valid value';
94             delete $errors{$key};
95             }
96              
97             foreach (keys %errors) {
98             my $key = $_;
99             s/_/ /g;
100             $r->template_args->{errors}{ucfirst($_)} = 'Please provide a valid value for this field';
101             $r->template_args->{errors}{$key} = 'Please provide a valid value for this field';
102             }
103              
104             undef $obj if $creating;
105              
106             die "do_update failed with error : $fatal" if ($fatal);
107             $r->template("edit");
108             } else {
109             $r->template("view");
110             }
111              
112             $r->objects( $obj ? [$obj] : []);
113             }
114              
115             # split out from do_edit to be reported by Mp::P::Trace
116             sub _do_update_or_create {
117             my ($self, $r, $obj, $required_cols, $ignored_cols) = @_;
118              
119             my $fatal;
120             my $creating = 0;
121              
122             my $h = $self->Untainter->new( %{$r->params} );
123              
124             # update or create
125             if ($obj) {
126             # We have something to edit
127             eval { $obj->update_from_cgi( $h => {
128             required => $required_cols,
129             ignore => $ignored_cols,
130             });
131             $obj->update(); # pos fix for bug 17132 'autoupdate required by do_edit'
132             };
133             $fatal = $@;
134             } else {
135             eval {
136             $obj = $self->create_from_cgi( $h => {
137             required => $required_cols,
138             ignore => $ignored_cols,
139             } );
140             };
141             $fatal = $@;
142             $creating++;
143             }
144             return $obj, $fatal, $creating;
145             }
146              
147             =head2 view
148              
149             This command shows the object using the view factory template.
150              
151             =cut
152              
153             sub view : Exported {
154             my ($self, $r) = @_;
155             $r->build_form_elements(0);
156             return;
157             }
158              
159              
160             =head2 delete
161              
162             Deprecated method that calls do_delete or a given classes delete method, please
163             use do_delete instead
164              
165             =head2 do_delete
166              
167             Unsuprisingly, this command causes a database record to be forever lost.
168              
169             This method replaces the, now deprecated, delete method provided in prior versions
170              
171             =cut
172              
173             sub delete : Exported {
174             my $self = shift;
175             my ($sub) = (caller(1))[3];
176             # So subclasses can still send delete down ...
177             $sub =~ /^(.+)::([^:]+)$/;
178             if ($1 ne "Maypole::Model::Base" && $2 ne "delete") {
179             $self->SUPER::delete(@_);
180             } else {
181             warn "Maypole::Model::CDBI::Base delete method is deprecated\n";
182             $self->do_delete(@_);
183             }
184             }
185              
186             sub do_delete : Exported {
187             my ( $self, $r ) = @_;
188             # FIXME: handle fatal error with exception
189             $_->SUPER::delete for @{ $r->objects || [] };
190             # $self->dbi_commit;
191             $r->objects( [ $self->retrieve_all ] );
192             $r->{template} = "list";
193             $self->list($r);
194             }
195              
196             =head2 search
197              
198             Deprecated searching method - use do_search instead.
199              
200             =head2 do_search
201              
202             This action method searches for database records, it replaces
203             the, now deprecated, search method previously provided.
204              
205             =cut
206              
207             sub search : Exported {
208             my $self = shift;
209             my ($sub) = (caller(1))[3];
210             # So subclasses can still send search down ...
211             if ($sub =~ /^(.+)::([^:]+)$/) {
212             return ($1 ne "Maypole::Model::Base" && $2 ne "search") ?
213             $self->SUPER::search(@_) : $self->do_search(@_);
214             } else {
215             $self->SUPER::search(@_);
216             }
217             }
218              
219             sub do_search : Exported {
220             my ( $self, $r ) = @_;
221             my %fields = map { $_ => 1 } $self->columns;
222             my $oper = "like"; # For now
223             my %params = %{ $r->{params} };
224             my %values = map { $_ => { $oper, $params{$_} } }
225             grep { defined $params{$_} && length ($params{$_}) && $fields{$_} }
226             keys %params;
227              
228             $r->template("list");
229             if ( !%values ) { return $self->list($r) }
230             my $order = $self->order($r);
231             $self = $self->do_pager($r);
232              
233             # FIXME: use pager info to get slice of iterator instead of all the objects as array
234              
235             $r->objects(
236             [
237             $self->search_where(
238             \%values, ( $order ? { order_by => $order } : () )
239             )
240             ]
241             );
242             $r->{template_args}{search} = 1;
243             }
244              
245             =head2 list
246              
247             The C method fills C<$r-Eobjects> with all of the
248             objects in the class. The results are paged using a pager.
249              
250             =cut
251              
252             sub list : Exported {
253             my ( $self, $r ) = @_;
254             my $order = $self->order($r);
255             $self = $self->do_pager($r);
256             if ($order) {
257             $r->objects( [ $self->retrieve_all_sorted_by($order) ] );
258             }
259             else {
260             $r->objects( [ $self->retrieve_all ] );
261             }
262             }
263              
264             ###############################################################################
265             # Helper methods
266              
267             =head1 Helper Methods
268              
269              
270             =head2 adopt
271              
272             This class method is passed the name of a model class that represents a table
273             and allows the master model class to do any set-up required.
274              
275             =cut
276              
277             sub adopt {
278             my ( $self, $child ) = @_;
279             $child->autoupdate(1);
280             if ( my $col = $child->stringify_column ) {
281             $child->columns( Stringify => $col );
282             }
283             }
284              
285              
286             =head2 related
287              
288             This method returns a list of has-many accessors. A brewery has many
289             beers, so C needs to return C.
290              
291             =cut
292              
293             sub related {
294             my ( $self, $r ) = @_;
295             return keys %{ $self->meta_info('has_many') || {} };
296             }
297              
298              
299             =head2 related_class
300              
301             Given an accessor name as a method, this function returns the class this accessor returns.
302              
303             =cut
304              
305             sub related_class {
306             my ( $self, $r, $accessor ) = @_;
307             my $meta = $self->meta_info;
308             my @rels = keys %$meta;
309             my $related;
310             foreach (@rels) {
311             $related = $meta->{$_}{$accessor};
312             last if $related;
313             }
314             return unless $related;
315              
316             my $mapping = $related->{args}->{mapping};
317             if ( $mapping and @$mapping ) {
318             return $related->{foreign_class}->meta_info('has_a')->{$$mapping[0]}->{foreign_class};
319             }
320             else {
321             return $related->{foreign_class};
322             }
323             }
324              
325             =head2 search_columns
326              
327             $class->search_columns;
328              
329             Returns a list of columns suitable for searching - used in factory templates, over-ridden in
330             classes. Provides same list as display_columns unless over-ridden.
331              
332             =cut
333              
334             sub search_columns {
335             my $class = shift;
336             return $class->display_columns;
337             }
338              
339              
340             =head2 related_meta
341              
342             $class->related_meta($col);
343              
344             Returns the hash ref of relationship meta info for a given column.
345              
346             =cut
347              
348             sub related_meta {
349             my ($self,$r, $accssr) = @_;
350             $self->_croak("You forgot to put the place holder for 'r' or forgot the accssr parameter") unless $accssr;
351             my $class_meta = $self->meta_info;
352             if (my ($rel_type) = grep { defined $class_meta->{$_}->{$accssr} }
353             keys %$class_meta)
354             { return $class_meta->{$rel_type}->{$accssr} };
355             }
356              
357              
358              
359             =head2 stringify_column
360              
361             Returns the name of the column to use when stringifying
362             and object.
363              
364             =cut
365              
366             sub stringify_column {
367             my $class = shift;
368             return (
369             $class->columns("Stringify"),
370             ( grep { /^(name|title)$/i } $class->columns ),
371             ( grep { /(name|title)/i } $class->columns ),
372             ( grep { !/id$/i } $class->primary_columns ),
373             )[0];
374             }
375              
376             =head2 do_pager
377              
378             Sets the pager template argument ($r->{template_args}{pager})
379             to a Class::DBI::Pager object based on the rows_per_page
380             value set in the configuration of the application.
381              
382             This pager is used via the pager macro in TT Templates, and
383             is also accessible via Mason.
384              
385             =cut
386              
387             sub do_pager {
388             my ( $self, $r ) = @_;
389             if ( my $rows = $r->config->rows_per_page ) {
390             return $r->{template_args}{pager} =
391             $self->pager( $rows, $r->query->{page} );
392             }
393             else { return $self }
394             }
395              
396              
397             =head2 order
398              
399             Returns the SQL order syntax based on the order parameter passed
400             to the request, and the valid columns.. i.e. 'title ASC' or 'date_created DESC'.
401              
402             $sql .= $self->order($r);
403              
404             If the order column is not a column of this table,
405             or an order argument is not passed, then the return value is undefined.
406              
407             Note: the returned value does not start with a space.
408              
409             =cut
410              
411             sub order {
412             my ( $self, $r ) = @_;
413             my %ok_columns = map { $_ => 1 } $self->columns;
414             my $q = $r->query;
415             my $order = $q->{order};
416             return unless $order and $ok_columns{$order};
417             $order .= ' DESC' if $q->{o2} and $q->{o2} eq 'desc';
418             return $order;
419             }
420              
421              
422             =head2 fetch_objects
423              
424             Returns 1 or more objects of the given class when provided with the request
425              
426             =cut
427              
428             sub fetch_objects {
429             my ($class, $r)=@_;
430             my @pcs = $class->primary_columns;
431             if ( $#pcs ) {
432             my %pks;
433             @pks{@pcs}=(@{$r->{args}});
434             return $class->retrieve( %pks );
435             }
436             return $class->retrieve( $r->{args}->[0] );
437             }
438              
439              
440             =head2 _isa_class
441              
442             Private method to return the class a column
443             belongs to that was inherited by an is_a relationship.
444             This should probably be public but need to think of API
445              
446             =cut
447              
448             sub _isa_class {
449             my ($class, $col) = @_;
450             $class->_croak( "Need a column for _isa_class." ) unless $col;
451             my $isaclass;
452             my $isa = $class->meta_info("is_a") || {};
453             foreach ( keys %$isa ) {
454             $isaclass = $isa->{$_}->foreign_class;
455             return $isaclass if ($isaclass->find_column($col));
456             }
457             return; # col not in a is_a class
458             }
459              
460              
461             # Thanks to dave baird -- form builder for these private functions
462             # sub _column_info {
463             sub _column_info {
464             my $self = shift;
465             my $dbh = $self->db_Main;
466              
467             my $meta; # The info we are after
468             my ($catalog, $schema) = (undef, undef);
469             # Dave is suspicious this (above undefs) could
470             # break things if driver useses this info
471              
472             my $original_metadata;
473             # '%' is a search pattern for columns - matches all columns
474             if ( my $sth = $dbh->column_info( $catalog, $schema, $self->table, '%' ) ) {
475             $dbh->errstr && die "Error getting column info sth: " . $dbh->errstr;
476             $self->COLUMN_INFO ($self->_hash_type_meta( $sth ));
477             } else {
478             $self->COLUMN_INFO ($self->_hash_typeless_meta( ));
479             }
480              
481             return $self->COLUMN_INFO;
482             }
483              
484             sub _hash_type_meta {
485             my ($self, $sth) = @_;
486             my $meta;
487             while ( my $row = $sth->fetchrow_hashref ) {
488             my $colname = $row->{COLUMN_NAME} || $row->{column_name};
489              
490             # required / nullable
491             $meta->{$colname}{nullable} = $row->{NULLABLE};
492             $meta->{$colname}{required} = ( $meta->{$colname}{nullable} == 0 ) ? 1 : 0;
493              
494             # default
495             if (defined $row->{COLUMN_DEF}) {
496             my $default = $row->{COLUMN_DEF};
497             $default =~ s/['"]?(.*?)['"]?::.*$/$1/;
498             $meta->{$colname}{default} = $default;
499             }else {
500             $meta->{$colname}{default} = '';
501             }
502              
503             # type
504             my $type = $row->{mysql_type_name} || $row->{type};
505             unless ($type) {
506             $type = $row->{TYPE_NAME};
507             if ($row->{COLUMN_SIZE}) {
508             $type .= "($row->{COLUMN_SIZE})";
509             }
510             }
511             $type =~ s/['"]?(.*)['"]?::.*$/$1/;
512             # Bool if tinyint
513             if ($type and $type =~ /^tinyint/i and $row->{COLUMN_SIZE} == 1) {
514             $type = 'BOOL';
515             }
516             $meta->{$colname}{type} = $type;
517              
518             # order
519             $meta->{$colname}{position} = $row->{ORDINAL_POSITION}
520             }
521             return $meta;
522             }
523              
524             # typeless db e.g. sqlite
525             sub _hash_typeless_meta {
526             my ( $self ) = @_;
527              
528             $self->set_sql( fb_meta_dummy => 'SELECT * FROM __TABLE__ WHERE 1=0' )
529             unless $self->can( 'sql_fb_meta_dummy' );
530              
531             my $sth = $self->sql_fb_meta_dummy;
532              
533             $sth->execute or die "Error executing column info: " . $sth->errstr;;
534              
535             # see 'Statement Handle Attributes' in the DBI docs for a list of available attributes
536             my $cols = $sth->{NAME};
537             my $types = $sth->{TYPE};
538             # my $sizes = $sth->{PRECISION}; # empty
539             # my $nulls = $sth->{NULLABLE}; # empty
540              
541             # we haven't actually fetched anything from the sth, so need to tell DBI we're not going to
542             $sth->finish;
543              
544             my $order = 0;
545             my $meta;
546             foreach my $col ( @$cols ) {
547             my $col_meta;
548             $col_meta->{nullable} = 1;
549             $col_meta->{required} = 0;
550             $col_meta->{default} = '';
551             $col_meta->{position} = $order++;
552             # type_name is taken literally from the schema, but is not actually used by sqlite,
553             # so it can be anything, e.g. varchar or varchar(xxx) or VARCHAR etc.
554             my $type = shift( @$types );
555             $col_meta->{type} = ($type =~ /(\w+)\((\w+)\)/) ? $1 :$type ;
556             $meta->{$col} = $col_meta;
557             }
558             return $meta;
559             }
560              
561             =head2 column_type
562              
563             my $type = $class->column_type('column_name');
564              
565             This returns the 'type' of this column (VARCHAR(20), BIGINT, etc.)
566             For now, it returns "BOOL" for tinyints.
567              
568             TODO :: TEST with enums
569              
570             =cut
571              
572             sub column_type {
573             my $class = shift;
574             my $colname = shift or die "Need a column for column_type";
575             $class->_column_info() unless (ref $class->COLUMN_INFO);
576              
577             if ($class->_isa_class($colname)) {
578             return $class->_isa_class($colname)->column_type($colname);
579             }
580             unless ( $class->find_column($colname) ) {
581             warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
582             return undef;
583             }
584             return $class->COLUMN_INFO->{$colname}{type};
585             }
586              
587             =head2 required_columns
588              
589             Accessor to get/set required columns for forms, validation, etc.
590              
591             Returns list of required columns. Accepts an array ref of column names.
592              
593             $class->required_columns([qw/foo bar baz/]);
594              
595             Allows you to specify the required columns for a class, over-riding any
596             assumptions and guesses made by Maypole.
597              
598             Any columns specified as required will no longer be 'nullable' or optional, and
599             any columns not specified as 'required' will be 'nullable' or optional.
600              
601             The default for a column is nullable, or whatever is discovered from database
602             schema.
603              
604             Use this instead of $config->{$table}{required_cols}
605              
606             Note : you need to setup the model class before calling this method.
607              
608             =cut
609              
610             sub required_columns {
611             my ($class, $columns) = @_;
612             $class->_column_info() unless (ref $class->COLUMN_INFO);
613             my $column_info = $class->COLUMN_INFO;
614              
615             if ($columns) {
616             # get the previously required columns
617             my %previously_required = map { $_ => 1} grep($column_info->{$_}{required}, keys %$column_info);
618              
619             # update each specified column as required
620             foreach my $colname ( @$columns ) {
621             # handle C::DBI::Rel::IsA
622             if ($class->_isa_class($colname)) {
623             $class->_isa_class($colname)->COLUMN_INFO->{$colname}{required} = 1
624             unless ($class->_isa_class($colname)->column_required);
625             next;
626             }
627             unless ( $class->find_column($colname) ) {
628             warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
629             next;
630             }
631             $column_info->{$colname}{required} = 1;
632             delete $previously_required{$colname};
633             }
634              
635             # no longer require any columns not specified
636             foreach my $colname ( keys %previously_required ) {
637             $column_info->{$colname}{required} = 0;
638             $column_info->{$colname}{nullable} = 1;
639             }
640              
641             # update column metadata
642             $class->COLUMN_INFO($column_info);
643             }
644              
645             return [ grep ($column_info->{$_}{required}, keys %$column_info) ] ;
646             }
647              
648             =head2 column_required
649              
650             Returns true if a column is required
651              
652             my $required = $class->column_required($column_name);
653              
654             Columns can be required by the application but not the database, but not the other way around,
655             hence there is also a column_nullable method which will tell you if the column is nullable
656             within the database itself.
657              
658             =cut
659              
660             sub column_required {
661             my ($class, $colname) = @_;
662             $colname or $class->_croak( "Need a column for column_required" );
663             $class->_column_info() unless ref $class->COLUMN_INFO;
664             if ($class->_isa_class($colname)) {
665             return $class->_isa_class($colname)->column_required($colname);
666             }
667             unless ( $class->find_column($colname) ) {
668             # handle non-existant columns
669             warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
670             return undef;
671             }
672             return $class->COLUMN_INFO->{$colname}{required} if ($class->COLUMN_INFO and $class->COLUMN_INFO->{$colname});
673             return 0;
674             }
675              
676             =head2 column_nullable
677              
678             Returns true if a column can be NULL within the underlying database and false if not.
679              
680             my $nullable = $class->column_nullable($column_name);
681              
682             Any columns that are not nullable will automatically be specified as required, you can
683             also specify nullable columns as required within your application.
684              
685             It is recomended you use column_required rather than column_nullable within your
686             application, this method is more useful if extending the model or handling your own
687             validation.
688              
689             =cut
690              
691             sub column_nullable {
692             my $class = shift;
693             my $colname = shift or $class->_croak( "Need a column for column_nullable" );
694              
695             $class->_column_info() unless ref $class->COLUMN_INFO;
696             if ($class->_isa_class($colname)) {
697             return $class->_isa_class($colname)->column_nullable($colname);
698             }
699             unless ( $class->find_column($colname) ) {
700             # handle non-existant columns
701             warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
702             return undef;
703             }
704             return $class->COLUMN_INFO->{$colname}{nullable} if ($class->COLUMN_INFO and $class->COLUMN_INFO->{$colname});
705             return 0;
706             }
707              
708             =head2 column_default
709              
710             Returns default value for column or the empty string.
711             Columns with NULL, CURRENT_TIMESTAMP, or Zeros( 0000-00...) for dates and times
712             have '' returned.
713              
714             =cut
715              
716             sub column_default {
717             my $class = shift;
718             my $colname = shift or $class->_croak( "Need a column for column_default");
719             $class->_column_info() unless (ref $class->COLUMN_INFO);
720             if ($class->_isa_class($colname)) {
721             return $class->_isa_class($colname)->column_default($colname);
722             }
723             unless ( $class->find_column($colname) ) {
724             warn "$colname is not a recognised column in this class ", ref $class || $class, "\n";
725             return undef;
726             }
727              
728             return $class->COLUMN_INFO->{$colname}{default} if ($class->COLUMN_INFO and $class->COLUMN_INFO->{$colname});
729             return;
730             }
731              
732             =head2 get_classmetadata
733              
734             Gets class meta data *excluding cgi input* for the passed in class or the
735             calling class. *NOTE* excludes cgi inputs. This method is handy to call from
736             templates when you need some metadata for a related class.
737              
738             =cut
739              
740             sub get_classmetadata {
741             my ($self, $class) = @_; # class is class we want data for
742             $class ||= $self;
743             $class = ref $class || $class;
744              
745             my %res;
746             $res{name} = $class;
747             $res{colnames} = {$class->column_names};
748             $res{columns} = [$class->display_columns];
749             $res{list_columns} = [$class->list_columns];
750             $res{moniker} = $class->moniker;
751             $res{plural} = $class->plural_moniker;
752             $res{table} = $class->table;
753             $res{column_metadata} = (ref $class->COLUMN_INFO) ? $class->COLUMN_INFO : $class->_column_info() ;
754             return \%res;
755             }
756              
757              
758             =head1 SEE ALSO
759              
760             L, L.
761              
762             =head1 AUTHOR
763              
764             Maypole is currently maintained by Aaron Trevena.
765              
766             =head1 AUTHOR EMERITUS
767              
768             Simon Cozens, C
769              
770             Simon Flack maintained Maypole from 2.05 to 2.09
771              
772             Sebastian Riedel, C maintained Maypole from 1.99_01 to 2.04
773              
774             =head1 LICENSE
775              
776             You may distribute this code under the same terms as Perl itself.
777              
778             =cut
779              
780             1;