]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Storage/CDBI.pm
added qw(:level) to CDBI.pm because we were getting empty log levels
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Storage / CDBI.pm
1 package OpenILS::Application::Storage::CDBI;
2 use base qw/Class::DBI/;
3 use Class::DBI;
4 use Class::DBI::AbstractSearch;
5
6 use OpenILS::Application::Storage::CDBI::actor;
7 use OpenILS::Application::Storage::CDBI::action;
8 use OpenILS::Application::Storage::CDBI::asset;
9 use OpenILS::Application::Storage::CDBI::authority;
10 use OpenILS::Application::Storage::CDBI::biblio;
11 use OpenILS::Application::Storage::CDBI::config;
12 use OpenILS::Application::Storage::CDBI::metabib;
13 use OpenILS::Application::Storage::CDBI::money;
14 use OpenILS::Application::Storage::CDBI::permission;
15 use OpenILS::Application::Storage::CDBI::container;
16
17 use JSON;
18 use OpenSRF::Utils::Logger qw(:level);
19 use OpenSRF::EX qw/:try/;
20
21 our $VERSION = undef;
22 my $log = 'OpenSRF::Utils::Logger';
23
24 sub child_init {
25         my $self = shift;
26
27         $log->debug("Creating ImaDBI Querys", DEBUG);
28         __PACKAGE__->set_sql( 'OILSFastSearch', <<"     SQL", 'Main');
29                 SELECT  %s
30                   FROM  %s
31                   WHERE %s = ?
32         SQL
33
34         __PACKAGE__->set_sql( 'OILSFastOrderedSearchLike', <<"  SQL", 'Main');
35                 SELECT  %s
36                   FROM  %s
37                   WHERE %s LIKE ?
38                   ORDER BY %s
39         SQL
40
41         __PACKAGE__->set_sql( 'OILSFastOrderedSearch', <<"      SQL", 'Main');
42                 SELECT  %s
43                   FROM  %s
44                   WHERE %s = ?
45                   ORDER BY %s
46         SQL
47
48         $log->debug("Calling Driver child_init", DEBUG);
49         $self->SUPER::child_init(@_);
50
51 }
52
53 sub fast_flesh_sth {
54         my $class = shift;
55         $class = ref($class) || $class;
56
57         my $field = shift;
58         my $value = shift;
59         my $order = shift;
60         my $like = shift;
61
62
63         if (!(defined($order) and ref($order) and ref($order) eq 'HASH')) {
64                 if (defined($value) and ref($value) and ref($value) eq 'HASH') {
65                         $order = $value;
66                         $value = undef;
67                 } else {
68                         $order = { order_by => $class->columns('Primary') }
69                 }
70         }
71
72         unless (defined $value) {
73                 $value = $field;
74                 ($field) = $class->columns('Primary');
75         }
76
77         unless (defined $field) {
78                 ($field) = $class->columns('Primary');
79         }
80
81         unless ($order->{order_by}) {
82                 $order = { order_by => $class->columns('Primary') }
83         }
84
85         my $fm_class = 'Fieldmapper::'.$class;
86         my $field_list = join ',', $class->columns('All');
87         
88         my $sth;
89         if (!$like) {
90                 $sth = $class->sql_OILSFastOrderedSearch( $field_list, $class->table, $field, $order->{order_by});
91         } else {
92                 $sth = $class->sql_OILSFastOrderedSearchLike( $field_list, $class->table, $field, $order->{order_by});
93         }
94         $sth->execute($value);
95         return $sth;
96 }
97
98 sub fast_flesh {
99         my $self = shift;
100         return map $class->construct($_), $self->fast_flesh_sth(@_)->fetchall_hash;
101 }
102
103 sub fast_fieldmapper {
104         my $self = shift;
105         my $id = shift;
106         my $col = shift;
107         my $like = shift;
108         my $options = shift;
109         my $class = ref($self) || $self;
110         my $fm_class = 'Fieldmapper::'.$class;
111         my @fms;
112         $log->debug("fast_fieldmapper() ==> Retrieving $fm_class", INTERNAL);
113         if ($like < 2) {
114                 for my $hash ($self->fast_flesh_sth( $col, "$id", { order_by => $col }, $like )->fetchall_hash) {
115                         my $fm = $fm_class->new;
116                         for my $field ( $fm_class->real_fields ) {
117                                 $fm->$field( $$hash{$field} );
118                         }
119                         push @fms, $fm;
120                 }
121         } else {
122                 my $search_type = 'search';
123                 if ($like == 2) {
124                         $search_type = 'search_fts'
125                 } elsif ($like == 3) {
126                         $search_type = 'search_regex'
127                 }
128
129                 for my $obj ($class->$search_type({ $col => $id}, $options)) {
130                         push @fms, $obj->to_fieldmapper;
131                 }
132         }
133         return @fms;
134 }
135
136 sub retrieve {
137         my $self = shift;
138         my $arg = shift;
139         if (ref($arg) &&
140                 (UNIVERSAL::isa($arg => 'Fieldmapper') ||
141                  UNIVERSAL::isa($arg => 'Class::DBI')) ) {
142                 my ($col) = $self->primary_column;
143                 $log->debug("Using field $col as the primary key", INTERNAL);
144                 $arg = $arg->$col;
145         } elsif (ref $arg) {
146                 my ($col) = $self->primary_column;
147                 $log->debug("Using field $col as the primary key", INTERNAL);
148                 $arg = $arg->{$col};
149         }
150                 
151         $log->debug("Retrieving $self with $arg", INTERNAL);
152         my $rec;
153         try {
154                 $rec = $self->SUPER::retrieve("$arg");
155         } catch Error with {
156                 $log->debug("Could not retrieve $self with $arg! -- ".shift(), DEBUG);
157                 return undef;
158         };
159         return $rec;
160 }
161
162 sub to_fieldmapper {
163         my $obj = shift;
164         my $class = ref($obj) || $obj;
165
166         my $fm_class = 'Fieldmapper::'.$class;
167         my $fm = $fm_class->new;
168
169         if (ref($obj)) {
170                 for my $field ( $fm->real_fields ) {
171                         $fm->$field( $obj->$field );
172                 }
173         }
174
175         return $fm;
176 }
177
178 sub merge {
179         my $self = shift;
180         my $search = shift;
181         my $arg = shift;
182
183         delete $$arg{$_} for (keys %$search);
184
185         $log->debug("CDBI->merge: \$search is $search (".ref($search)." : ".join(',',map{"$_ => $$search{$_}"}keys(%$search)).")",DEBUG);
186         $log->debug("CDBI->merge: \$arg is $arg (".ref($arg)." : ".join(',',map{"$_ => $$arg{$_}"}keys(%$arg)).")",DEBUG);
187
188         my @objs = ($self);
189         @objs = $self->search_where($search) unless (ref $self);
190
191         if (@objs == 1) {
192                 return $objs[0]->update($arg);
193         } elsif (@objs == 0) {
194                 return $self->create({%$search,%$arg});
195         } else {
196                 throw OpenSRF::EX::WARN ("Non-unique search key for merge.  Perhaps you meant to use remote_update?");
197         }
198 }
199
200 sub remote_update {
201         my $self = shift;
202         my $search = shift;
203         my $arg = shift;
204
205         delete $$arg{$_} for (keys %$search);
206
207         $log->debug("CDBI->remote_update: \$search is $search (".ref($search)." : ".join(',',map{"$_ => $$search{$_}"}keys(%$search)).")",DEBUG);
208         $log->debug("CDBI->remote_update: \$arg is $arg (".ref($arg)." : ".join(',',map{"$_ => $$arg{$_}"}keys(%$arg)).")",DEBUG);
209
210 #       my @objs = $self->search_where($search);
211 #       throw OpenSRF::EX::WARN ("No objects found for remote_update.  Perhaps you meant to use merge?")
212 #               if (@objs == 0);
213
214 #       $_->update($arg) for (@objs);
215 #       return scalar(@objs);
216
217         my @finds = sort keys %$search;
218         my @sets = sort keys %$arg;
219
220         my @find_vals = @$search{@finds};
221         my @set_vals = @$arg{@sets};
222
223         my $sql = 'UPDATE %s SET %s WHERE %s';
224
225         my $table = $self->table;
226         my $set = join(', ', map { "$_=?" } @sets);
227         my $where = join(', ', map { "$_=?" } @finds);
228
229         my $sth = $self->db_Main->prepare(sprintf($sql, $table, $set, $where));
230         $sth->execute(@set_vals,@find_vals);
231         return $sth->rows;
232
233 }
234
235 sub create {
236         my $self = shift;
237         my $arg = shift;
238
239         $log->debug("CDBI->create: \$arg is $arg (".ref($arg)." : ".JSON->perl2JSON($arg).")",DEBUG);
240
241         if (ref($arg) && UNIVERSAL::isa($arg => 'Fieldmapper')) {
242                 return $self->create_from_fieldmapper($arg,@_);
243         }
244
245         return $self->SUPER::create($arg,@_);
246 }
247
248 sub create_from_fieldmapper {
249         my $obj = shift;
250         my $fm = shift;
251         my @params = @_;
252
253         $log->debug("Creating node of type ".ref($fm), DEBUG);
254
255         my $class = ref($obj) || $obj;
256         my ($primary) = $class->columns('Primary');
257
258         if (ref($fm) &&UNIVERSAL::isa($fm => 'Fieldmapper')) {
259                 my %hash = map { defined $fm->$_ ?
260                                         ($_ => $fm->$_) :
261                                         ()
262                                 } grep { $_ ne $primary } $class->columns('All');
263
264                 if ($class->find_column( 'last_xact_id' )) {
265                         my $xact_id = $class->current_xact_id;
266                         throw Error unless ($xact_id);
267                         $hash{last_xact_id} = $xact_id;
268                 }
269
270                 return $class->create( \%hash, @params );
271         } else {
272                 return undef;
273         }
274 }
275
276 sub delete {
277         my $self = shift;
278         my $arg = shift;
279         my $orig = $self;
280
281         my $class = ref($self) || $self;
282
283         $self = $self->retrieve($arg) if (!ref($self));
284         unless (defined $self) {
285                 $log->debug("ARG! Couldn't retrieve record ".$arg->id, DEBUG);
286                 throw OpenSRF::EX::WARN ("ARG! Couldn't retrieve record ");
287         }
288
289         if ($class->find_column( 'last_xact_id' )) {
290                 my $xact_id = $self->current_xact_id;
291                 
292                 throw Error ("Deleting from $class requires a transaction be established")
293                         unless ($xact_id);
294                 
295                 throw Error ("The row you are attempting to delete has been changed since you read it")
296                         unless ( $orig->last_xact_id eq $self->last_xact_id);
297
298                 $self->last_xact_id( $class->current_xact_id );
299                 $self->SUPER::update;
300         }
301
302         $self->SUPER::delete;
303
304         return 1;
305 }
306
307 sub debug_object {
308         my $obj = shift;
309         my $string = '';
310
311         $string .= "Object type:\t".ref($obj)."\n";
312         $string .= "Object string:\t$obj\n";
313
314         if (ref($obj) && UNIVERSAL::isa($obj => 'Fieldmapper')) {
315                 $string .= "Object fields:\n";
316                 for my $col ($obj->real_fields()) {
317                         $string .= "\t$col\t=> ".$obj->$col."\n";
318                 }
319         } elsif (ref($obj) && UNIVERSAL::isa($obj => 'Class::DBI')) {
320                 $string .= "Object cols:\n";
321                 for my $col ($obj->columns('All')) {
322                         $string .= "\t$col\t=> ".$obj->$col."\n";
323                 }
324         } elsif (ref($obj) && UNIVERSAL::isa($obj => 'HASH')) {
325                 $string .= "Object keys and vals:\n";
326                 for my $col (keys %$obj) {
327                         $string .= "\t$col\t=> $$obj{$col}\n";
328                 }
329         }
330
331         $string .= "\n";
332         
333         $log->debug($string,DEBUG);
334 }
335
336
337 sub update {
338         my $self = shift;
339         my $arg = shift;
340
341         $log->debug("Attempting to update using $arg", DEBUG) if ($arg);
342
343         if (ref($arg)) {
344                 $self = $self->modify_from_fieldmapper($arg);
345                 $log->debug("Modification of $self seems to have failed....", DEBUG);
346                 return undef unless (defined $self);
347         }
348
349         $log->debug("Calling Class::DBI->update on modified object $self", DEBUG);
350
351         debug_object($self);
352
353         return $self->SUPER::update if ($self->is_changed);
354         return 0;
355 }
356
357 sub modify_from_fieldmapper {
358         my $obj = shift;
359         my $fm = shift;
360         my $orig = $obj;
361
362         debug_object($obj);
363         debug_object($fm);
364
365         $log->debug("Modifying object using fieldmapper", DEBUG);
366
367         my $class = ref($obj) || $obj;
368         my ($primary) = $class->columns('Primary');
369
370
371         if (!ref($obj)) {
372                 $obj = $class->retrieve($fm);
373                 debug_object($obj);
374                 unless ($obj) {
375                         $log->debug("Retrieve of $class using $fm (".$fm->id.") failed! -- ".shift(), ERROR);
376                         throw OpenSRF::EX::WARN ("No $class with id of ".$fm->id."!!");
377                 }
378         }
379
380         my %hash;
381         
382         if (ref($fm) and UNIVERSAL::isa($fm => 'Fieldmapper')) {
383                 %hash = map { defined $fm->$_ ?
384                                 ($_ => ''.$fm->$_) :
385                                 ()
386                         } grep { $_ ne $primary } $class->columns('All');
387         } else {
388                 %hash = %{$fm};
389         }
390
391         my $au = $obj->autoupdate;
392         $obj->autoupdate(0);
393         
394         debug_object($obj);
395
396         for my $field ( keys %hash ) {
397                 $obj->$field( $hash{$field} ) if ($obj->$field ne $hash{$field});
398                 $log->debug("Setting field $field on $obj to $hash{$field}",INTERNAL);
399         }
400
401         if ($class->find_column( 'last_xact_id' ) and $obj->is_changed) {
402                 my ($xact_id) = OpenILS::Application::Storage->method_lookup('open-ils.storage.transaction.current')->run();
403                 throw Error ("Updating $class requires a transaction be established")
404                         unless ($xact_id);
405                 throw Error ("The row you are attempting to delete has been changed since you read it")
406                         unless ( $fm->last_xact_id eq $obj->last_xact_id);
407                 $obj->last_xact_id( $xact_id );
408         } else {
409                 $obj->autoupdate($au)
410         }
411
412         return $obj;
413 }
414
415
416
417         #-------------------------------------------------------------------------------
418         actor::user->has_a( home_ou => 'actor::org_unit' );
419         actor::user->has_a( card => 'actor::card' );
420         actor::user->has_a( standing => 'config::standing' );
421         actor::user->has_a( profile => 'actor::profile' );
422         actor::user->has_a( mailing_address => 'actor::user_address' );
423         actor::user->has_a( billing_address => 'actor::user_address' );
424         actor::user->has_a( ident_type => 'config::identification_type' );
425         actor::user->has_a( ident_type2 => 'config::identification_type' );
426         actor::user->has_a( net_access_level => 'config::net_access_level' );
427
428         actor::user_address->has_a( usr => 'actor::user' );
429         
430         actor::card->has_a( usr => 'actor::user' );
431         
432         actor::org_unit->has_a( parent_ou => 'actor::org_unit' );
433         actor::org_unit->has_a( ou_type => 'actor::org_unit_type' );
434         #actor::org_unit->has_a( address => 'actor::org_address' );
435
436         actor::stat_cat_entry->has_a( stat_cat => 'actor::stat_cat' );
437         actor::stat_cat->has_many( entries => 'actor::stat_cat_entry' );
438         actor::stat_cat_entry_user_map->has_a( stat_cat => 'actor::stat_cat' );
439         actor::stat_cat_entry_user_map->has_a( stat_cat_entry => 'actor::stat_cat_entry' );
440         actor::stat_cat_entry_user_map->has_a( target_usr => 'actor::user' );
441
442         asset::stat_cat_entry->has_a( stat_cat => 'asset::stat_cat' );
443         asset::stat_cat->has_many( entries => 'asset::stat_cat_entry' );
444         asset::stat_cat_entry_copy_map->has_a( stat_cat => 'asset::stat_cat' );
445         asset::stat_cat_entry_copy_map->has_a( stat_cat_entry => 'asset::stat_cat_entry' );
446         asset::stat_cat_entry_copy_map->has_a( owning_copy => 'asset::copy' );
447
448         action::survey_response->has_a( usr => 'actor::user' );
449         action::survey_response->has_a( survey => 'action::survey' );
450         action::survey_response->has_a( question => 'action::survey_question' );
451         action::survey_response->has_a( answer => 'action::survey_answer' );
452
453         action::survey_question->has_a( survey => 'action::survey' );
454
455         action::survey_answer->has_a( question => 'action::survey' );
456
457         asset::copy_note->has_a( owning_copy => 'asset::copy' );
458
459         actor::user->has_many( stat_cat_entries => [ 'actor::stat_cat_entry_user_map' => 'stat_cat_entry' ] );
460         actor::user->has_many( stat_cat_entry_user_maps => 'actor::stat_cat_entry_user_map' );
461
462         asset::copy->has_many( stat_cat_entries => [ 'asset::stat_cat_entry_copy_map' => 'stat_cat_entry' ] );
463         asset::copy->has_many( stat_cat_entry_copy_maps => 'asset::stat_cat_entry_copy_map' );
464
465         asset::copy->has_a( call_number => 'asset::call_number' );
466         asset::copy->has_a( creator => 'actor::user' );
467         asset::copy->has_a( editor => 'actor::user' );
468         asset::copy->has_a( status => 'config::copy_status' );
469         asset::copy->has_a( location => 'asset::copy_location' );
470         asset::copy->has_a( circ_lib => 'actor::org_unit' );
471
472         asset::call_number_note->has_a( call_number => 'asset::call_number' );
473
474         asset::call_number->has_a( record => 'biblio::record_entry' );
475         asset::call_number->has_a( creator => 'actor::user' );
476         asset::call_number->has_a( editor => 'actor::user' );
477
478         authority::record_note->has_a( record => 'authority::record_entry' );
479         biblio::record_note->has_a( record => 'biblio::record_entry' );
480         
481         authority::record_entry->has_a( creator => 'actor::user' );
482         authority::record_entry->has_a( editor => 'actor::user' );
483         biblio::record_entry->has_a( creator => 'actor::user' );
484         biblio::record_entry->has_a( editor => 'actor::user' );
485         
486         metabib::metarecord->has_a( master_record => 'biblio::record_entry' );
487         
488         authority::record_descriptor->has_a( record => 'authority::record_entry' );
489         metabib::record_descriptor->has_a( record => 'biblio::record_entry' );
490         
491         authority::full_rec->has_a( record => 'authority::record_entry' );
492         metabib::full_rec->has_a( record => 'biblio::record_entry' );
493         
494         metabib::title_field_entry->has_a( source => 'biblio::record_entry' );
495         metabib::title_field_entry->has_a( field => 'config::metabib_field' );
496         
497         metabib::author_field_entry->has_a( source => 'biblio::record_entry' );
498         metabib::author_field_entry->has_a( field => 'config::metabib_field' );
499         
500         metabib::subject_field_entry->has_a( source => 'biblio::record_entry' );
501         metabib::subject_field_entry->has_a( field => 'config::metabib_field' );
502         
503         metabib::keyword_field_entry->has_a( source => 'biblio::record_entry' );
504         metabib::keyword_field_entry->has_a( field => 'config::metabib_field' );
505         
506         metabib::series_field_entry->has_a( source => 'biblio::record_entry' );
507         metabib::series_field_entry->has_a( field => 'config::metabib_field' );
508         
509         metabib::metarecord_source_map->has_a( metarecord => 'metabib::metarecord' );
510         metabib::metarecord_source_map->has_a( source => 'biblio::record_entry' );
511
512         action::circulation->has_a( usr => 'actor::user' );
513         action::circulation->has_a( target_copy => 'asset::copy' );
514         action::circulation->has_a( circ_lib => 'actor::org_unit' );
515
516         money::billable_transaction->has_a( usr => 'actor::user' );
517         
518         
519         #-------------------------------------------------------------------------------
520         actor::user->has_many( survey_responses => 'action::survey_response' );
521         actor::user->has_many( addresses => 'actor::user_address' );
522         actor::user->has_many( cards => 'actor::card' );
523
524         actor::org_unit->has_many( users => 'actor::user' );
525         actor::profile->has_many( users => 'actor::user' );
526
527         action::survey->has_many( questions => 'action::survey_question' );
528         action::survey->has_many( responses => 'action::survey_response' );
529         
530         action::survey_question->has_many( answers => 'action::survey_answer' );
531         action::survey_question->has_many( responses => 'action::survey_response' );
532
533         action::survey_answer->has_many( responses => 'action::survey_response' );
534
535         asset::copy->has_many( notes => 'asset::copy_note' );
536         asset::call_number->has_many( copies => 'asset::copy' );
537         asset::call_number->has_many( notes => 'asset::call_number_note' );
538
539         authority::record_entry->has_many( record_descriptor => 'authority::record_descriptor' );
540         authority::record_entry->has_many( notes => 'authority::record_note' );
541
542         biblio::record_entry->has_many( record_descriptor => 'metabib::record_descriptor' );
543         biblio::record_entry->has_many( notes => 'biblio::record_note' );
544         biblio::record_entry->has_many( call_numbers => 'asset::call_number' );
545         biblio::record_entry->has_many( full_record_entries => 'metabib::full_rec' );
546         biblio::record_entry->has_many( title_field_entries => 'metabib::title_field_entry' );
547         biblio::record_entry->has_many( author_field_entries => 'metabib::author_field_entry' );
548         biblio::record_entry->has_many( subject_field_entries => 'metabib::subject_field_entry' );
549         biblio::record_entry->has_many( keyword_field_entries => 'metabib::keyword_field_entry' );
550         biblio::record_entry->has_many( series_field_entries => 'metabib::series_field_entry' );
551
552         metabib::metarecord->has_many( source_records => [ 'metabib::metarecord_source_map' => 'source'] );
553
554         money::billable_transaction->has_many( billings => 'money::billing' );
555         money::billable_transaction->has_many( payments => 'money::payment' );
556
557         money::grocery->has_many( billings => 'money::billing' );
558         money::grocery->has_many( payments => 'money::payment' );
559
560         money::billing->has_a( xact => 'money::billable_transaction' );
561         money::payment->has_a( xact => 'money::billable_transaction' );
562
563         money::cash_payment->has_a( xact => 'money::billable_transaction' );
564         money::cash_payment->has_a( accepting_usr => 'actor::user' );
565
566         money::check_payment->has_a( xact => 'money::billable_transaction' );
567         money::check_payment->has_a( accepting_usr => 'actor::user' );
568
569         money::credit_card_payment->has_a( xact => 'money::billable_transaction' );
570         money::credit_card_payment->has_a( accepting_usr => 'actor::user' );
571
572         money::forgive_payment->has_a( xact => 'money::billable_transaction' );
573         money::forgive_payment->has_a( accepting_usr => 'actor::user' );
574
575         money::work_payment->has_a( xact => 'money::billable_transaction' );
576         money::work_payment->has_a( accepting_usr => 'actor::user' );
577
578         money::credit_payment->has_a( xact => 'money::billable_transaction' );
579         money::credit_payment->has_a( accepting_usr => 'actor::user' );
580
581         permission::grp_tree->has_a( parent => 'permission::grp_tree' );
582
583         permission::grp_perm_map->has_a( grp => 'permission::grp_tree' );
584         permission::grp_perm_map->has_a(  perm => 'permission::perm_list' );
585         permission::grp_perm_map->has_a(  depth => 'actor::org_unit_type' );
586         
587         permission::usr_perm_map->has_a( usr => 'actor::user' );
588         permission::usr_perm_map->has_a(  perm => 'permission::perm_list' );
589         permission::usr_perm_map->has_a(  depth => 'actor::org_unit_type' );
590         
591         permission::usr_grp_map->has_a(  usr => 'actor::user' );
592         permission::usr_grp_map->has_a(  grp => 'permission::grp_tree' );
593
594         action::hold_notification->has_a(  hold => 'action::hold_request' );
595         
596         action::hold_copy_map->has_a(  hold => 'action::hold_request' );
597         action::hold_copy_map->has_a(  target_copy => 'asset::copy' );
598
599         action::hold_request->has_a(  current_copy => 'asset::copy' );
600         action::hold_request->has_a(  requestor => 'actor::user' );
601         action::hold_request->has_a(  usr => 'actor::user' );
602         action::hold_request->has_a(  pickup_lib => 'actor::org_unit' );
603
604         action::hold_request->has_many(  notifications => 'action::hold_notification' );
605         action::hold_request->has_many(  copy_maps => 'action::hold_copy_map' );
606
607         asset::copy->has_many(  hold_maps => 'action::hold_copy_map' );
608
609 1;