]> git.decadent.org.uk Git - maypole.git/blob - lib/Maypole/Model/CDBI.pm
df8d6c8b210094f3bc23703ae8e0a1f8bc522248
[maypole.git] / lib / Maypole / Model / CDBI.pm
1 package Maypole::Model::CDBI;
2 use base qw(Maypole::Model::Base Class::DBI);
3 use Class::DBI::AsForm;
4 # use Maypole::Form::CDBI;
5 use CGI::Untaint;
6 # use Maypole::Form;
7
8 use Class::DBI::FromCGI;
9 use Class::DBI::Loader;
10 use Class::DBI::AbstractSearch;
11 use Class::DBI::Plugin::RetrieveAll;
12 use Class::DBI::Pager;
13
14 use Lingua::EN::Inflect::Number qw(to_PL);
15
16 use strict;
17
18 =head1 NAME
19
20 Maypole::Model::CDBI - Model class based on Class::DBI
21
22 =head1 DESCRIPTION
23
24 This is a master model class which uses L<Class::DBI> to do all the hard
25 work of fetching rows and representing them as objects. It is a good
26 model to copy if you're replacing it with other database abstraction
27 modules.
28
29 It implements a base set of methods required for a Maypole Data Model.
30 See L<Maypole::Model::Base> for these:
31
32 =over 4
33
34 =item adopt
35
36 =item class_of
37
38 =item do_edit
39
40 =item list
41
42 =item related
43
44 =item setup_database
45
46 =item fetch_objects
47
48 =back 
49
50 =head1 Additional Actions
51
52 =over 
53
54 =item delete
55
56 Unsuprisingly, this command causes a database record to be forever lost.
57
58 =item search
59
60 The search action 
61
62 =back
63
64 =head1 Helper Methods
65
66 =over 
67
68 =item order
69
70 =item stringify_column
71
72 =item do_pager
73
74 =item related_class
75
76 Given an accessor name as a method, this function returns the class this accessor returns.
77
78 =back
79
80 =cut
81
82 sub related {
83     my ( $self, $r ) = @_;
84     return keys %{ $self->meta_info('has_many') || {} };
85 }
86
87 sub related_class {
88      my ( $self, $r, $accessor ) = @_;
89      my $meta = $self->meta_info;
90      my @rels = keys %$meta;
91      my $related;
92      foreach (@rels) {
93          $related = $meta->{$_}{$accessor};
94          last if $related;
95      }
96      return unless $related;
97
98      my $mapping = $related->{args}->{mapping};
99      if ( $mapping and @$mapping ) {
100        return $related->{foreign_class}->meta_info('has_a')->{$$mapping[0]}->{foreign_class};
101      }
102      else {
103          return $related->{foreign_class};
104      }
105  }
106
107
108 sub do_edit : Exported {
109     my ( $self, $r ) = @_;
110     my $h        = CGI::Untaint->new( %{ $r->{params} } );
111     my $creating = 0;
112     my ($obj) = @{ $r->objects || [] };
113     my $fatal;
114     if ($obj) {
115         # We have something to edit
116         eval {
117             $obj->update_from_cgi( $h =>
118                 { required => $r->{config}{ $r->{table} }{required_cols} || [], }
119             );
120         };
121         $fatal = $@;
122     }
123     else {
124         eval {
125             $obj =
126                 $self->create_from_cgi( $h =>
127                     { required => $r->{config}{ $r->{table} }{required_cols} || [], }
128             );
129         };
130         if ($fatal = $@) {
131             warn "$fatal" if $r->debug;
132         }
133         $creating++;
134     }
135     if ( my %errors = $fatal ? (FATAL => $fatal) : $obj->cgi_update_errors ) {
136
137         # Set it up as it was:
138         $r->{template_args}{cgi_params} = $r->{params};
139         $r->{template_args}{errors}     = \%errors;
140
141         undef $obj if $creating;
142         $r->template("edit");
143     }
144     else {
145         $r->{template} = "view";
146     }
147     $r->objects( $obj ? [$obj] : []);
148 }
149
150 sub delete : Exported {
151     return shift->SUPER::delete(@_) if caller ne "Maypole::Model::Base";
152     my ( $self, $r ) = @_;
153     $_->SUPER::delete for @{ $r->objects || [] };
154     $r->objects( [ $self->retrieve_all ] );
155     $r->{template} = "list";
156     $self->list($r);
157 }
158
159 sub stringify_column {
160     my $class = shift;
161     return (
162         $class->columns("Stringify"),
163         ( grep { /^(name|title)$/i } $class->columns ),
164         ( grep { /(name|title)/i } $class->columns ),
165         ( grep { !/id$/i } $class->primary_columns ),
166     )[0];
167 }
168
169 sub adopt {
170     my ( $self, $child ) = @_;
171     $child->autoupdate(1);
172     if ( my $col = $child->stringify_column ) {
173         $child->columns( Stringify => $col );
174     }
175 }
176
177 sub search : Exported {
178     return shift->SUPER::search(@_) if caller ne "Maypole::Model::Base";
179
180     # A real CDBI search.
181     my ( $self, $r ) = @_;
182     my %fields = map { $_ => 1 } $self->columns;
183     my $oper   = "like";                                # For now
184     my %params = %{ $r->{params} };
185     my %values = map { $_ => { $oper, $params{$_} } }
186       grep { defined $params{$_} && length ($params{$_}) && $fields{$_} }
187       keys %params;
188
189     $r->template("list");
190     if ( !%values ) { return $self->list($r) }
191     my $order = $self->order($r);
192     $self = $self->do_pager($r);
193     $r->objects(
194         [
195             $self->search_where(
196                 \%values, ( $order ? { order_by => $order } : () )
197             )
198         ]
199     );
200     $r->{template_args}{search} = 1;
201 }
202
203 sub do_pager {
204     my ( $self, $r ) = @_;
205     if ( my $rows = $r->config->rows_per_page ) {
206         return $r->{template_args}{pager} =
207           $self->pager( $rows, $r->query->{page} );
208     }
209     else { return $self }
210 }
211
212 sub order {
213     my ( $self, $r ) = @_;
214     my %ok_columns = map { $_ => 1 } $self->columns;
215     my $q = $r->query;
216     my $order = $q->{order};
217     return unless $order and $ok_columns{$order};
218     $order .= ' DESC' if $q->{o2} and $q->{o2} eq 'desc';
219     return $order;
220 }
221
222 sub list : Exported {
223     my ( $self, $r ) = @_;
224     my $order = $self->order($r);
225     $self = $self->do_pager($r);
226     if ($order) {
227         $r->objects( [ $self->retrieve_all_sorted_by($order) ] );
228     }
229     else {
230         $r->objects( [ $self->retrieve_all ] );
231     }
232 }
233
234 sub setup_database {
235     my ( $class, $config, $namespace, $dsn, $u, $p, $opts ) = @_;
236     $dsn  ||= $config->dsn;
237     $u    ||= $config->user;
238     $p    ||= $config->pass;
239     $opts ||= $config->opts;
240     $config->dsn($dsn);
241     warn "No DSN set in config" unless $dsn;
242     $config->loader || $config->loader(
243         Class::DBI::Loader->new(
244             namespace => $namespace,
245             dsn       => $dsn,
246             user      => $u,
247             password  => $p,
248             %$opts,
249         )
250     );
251     $config->{classes} = [ $config->{loader}->classes ];
252     $config->{tables}  = [ $config->{loader}->tables ];
253     warn( 'Loaded tables: ' . join ',', @{ $config->{tables} } )
254       if $namespace->debug;
255 }
256
257 sub class_of {
258     my ( $self, $r, $table ) = @_;
259     return $r->config->loader->_table2class($table); # why not find_class ?
260 }
261
262 sub fetch_objects {
263     my ($class, $r)=@_;
264     my @pcs = $class->primary_columns;
265     if ( $#pcs ) {
266     my %pks;
267         @pks{@pcs}=(@{$r->{args}});
268         return $class->retrieve( %pks );
269     }
270     return $class->retrieve( $r->{args}->[0] );
271 }
272
273 1;