refactored copy targeter to use new JS copy tester; adjusted object relationships...
[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 = 1;
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                 $objs[0]->update($arg);
193                 return $objs[0];
194         } elsif (@objs == 0) {
195                 return $self->create({%$search,%$arg});
196         } else {
197                 throw OpenSRF::EX::WARN ("Non-unique search key for merge.  Perhaps you meant to use remote_update?");
198         }
199 }
200
201 sub remote_update {
202         my $self = shift;
203         my $search = shift;
204         my $arg = shift;
205
206         delete $$arg{$_} for (keys %$search);
207
208         $log->debug("CDBI->remote_update: \$search is $search (".ref($search)." : ".join(',',map{"$_ => $$search{$_}"}keys(%$search)).")",DEBUG);
209         $log->debug("CDBI->remote_update: \$arg is $arg (".ref($arg)." : ".join(',',map{"$_ => $$arg{$_}"}keys(%$arg)).")",DEBUG);
210
211 #       my @objs = $self->search_where($search);
212 #       throw OpenSRF::EX::WARN ("No objects found for remote_update.  Perhaps you meant to use merge?")
213 #               if (@objs == 0);
214
215 #       $_->update($arg) for (@objs);
216 #       return scalar(@objs);
217
218         my @finds = sort keys %$search;
219         my @sets = sort keys %$arg;
220
221         my @find_vals = @$search{@finds};
222         my @set_vals = @$arg{@sets};
223
224         my $sql = 'UPDATE %s SET %s WHERE %s';
225
226         my $table = $self->table;
227         my $set = join(', ', map { "$_=?" } @sets);
228         my $where = join(', ', map { "$_=?" } @finds);
229
230         my $sth = $self->db_Main->prepare(sprintf($sql, $table, $set, $where));
231         $sth->execute(@set_vals,@find_vals);
232         return $sth->rows;
233
234 }
235
236 sub create {
237         my $self = shift;
238         my $arg = shift;
239
240         $log->debug("CDBI->create: \$arg is $arg (".ref($arg)." : ".JSON->perl2JSON($arg).")",DEBUG);
241
242         if (ref($arg) && UNIVERSAL::isa($arg => 'Fieldmapper')) {
243                 return $self->create_from_fieldmapper($arg,@_);
244         }
245
246         return $self->SUPER::create($arg,@_);
247 }
248
249 sub create_from_fieldmapper {
250         my $obj = shift;
251         my $fm = shift;
252         my @params = @_;
253
254         $log->debug("Creating node of type ".ref($fm), DEBUG);
255
256         my $class = ref($obj) || $obj;
257         my ($primary) = $class->columns('Primary');
258
259         if (ref($fm) &&UNIVERSAL::isa($fm => 'Fieldmapper')) {
260                 my %hash = map { defined $fm->$_ ?
261                                         ($_ => $fm->$_) :
262                                         ()
263                                 } grep { $_ ne $primary } $class->columns('All');
264
265                 if ($class->find_column( 'last_xact_id' )) {
266                         my $xact_id = $class->current_xact_id;
267                         throw Error unless ($xact_id);
268                         $hash{last_xact_id} = $xact_id;
269                 }
270
271                 return $class->create( \%hash, @params );
272         } else {
273                 return undef;
274         }
275 }
276
277 sub delete {
278         my $self = shift;
279         my $arg = shift;
280         my $orig = $self;
281
282         my $class = ref($self) || $self;
283
284         $self = $self->retrieve($arg) if (!ref($self));
285         unless (defined $self) {
286                 $log->debug("ARG! Couldn't retrieve record ".$arg->id, DEBUG);
287                 throw OpenSRF::EX::WARN ("ARG! Couldn't retrieve record ");
288         }
289
290         if ($class->find_column( 'last_xact_id' )) {
291                 my $xact_id = $self->current_xact_id;
292                 
293                 throw Error ("Deleting from $class requires a transaction be established")
294                         unless ($xact_id);
295                 
296                 throw Error ("The row you are attempting to delete has been changed since you read it")
297                         unless ( $orig->last_xact_id eq $self->last_xact_id);
298
299                 $self->last_xact_id( $class->current_xact_id );
300                 $self->SUPER::update;
301         }
302
303         $self->SUPER::delete;
304
305         return 1;
306 }
307
308 sub debug_object {
309         my $obj = shift;
310         my $string = '';
311
312         $string .= "Object type:\t".ref($obj)."\n";
313         $string .= "Object string:\t$obj\n";
314
315         if (ref($obj) && UNIVERSAL::isa($obj => 'Fieldmapper')) {
316                 $string .= "Object fields:\n";
317                 for my $col ($obj->real_fields()) {
318                         $string .= "\t$col\t=> ".$obj->$col."\n";
319                 }
320         } elsif (ref($obj) && UNIVERSAL::isa($obj => 'Class::DBI')) {
321                 $string .= "Object cols:\n";
322                 for my $col ($obj->columns('All')) {
323                         $string .= "\t$col\t=> ".$obj->$col."\n";
324                 }
325         } elsif (ref($obj) && UNIVERSAL::isa($obj => 'HASH')) {
326                 $string .= "Object keys and vals:\n";
327                 for my $col (keys %$obj) {
328                         $string .= "\t$col\t=> $$obj{$col}\n";
329                 }
330         }
331
332         $string .= "\n";
333         
334         $log->debug($string,DEBUG);
335 }
336
337
338 sub update {
339         my $self = shift;
340         my $arg = shift;
341
342         $log->debug("Attempting to update using $arg", DEBUG) if ($arg);
343
344         if (ref($arg)) {
345                 $self = $self->modify_from_fieldmapper($arg);
346                 unless (defined $self) {
347                         $log->debug("Modification of $arg seems to have failed....", DEBUG);
348                         return undef;
349                 }
350         }
351
352         $log->debug("Calling Class::DBI->update on modified object $self", DEBUG);
353
354         #debug_object($self);
355
356         return $self->SUPER::update if ($self->is_changed);
357         return 0;
358 }
359
360 sub modify_from_fieldmapper {
361         my $obj = shift;
362         my $fm = shift;
363         my $orig = $obj;
364
365         #debug_object($obj);
366         #debug_object($fm);
367
368         $log->debug("Modifying object using fieldmapper", DEBUG);
369
370         my $class = ref($obj) || $obj;
371         my ($primary) = $class->columns('Primary');
372
373
374         if (!ref($obj)) {
375                 $obj = $class->retrieve($fm);
376                 #debug_object($obj);
377                 unless ($obj) {
378                         $log->debug("Retrieve of $class using $fm (".$fm->id.") failed! -- ".shift(), ERROR);
379                         throw OpenSRF::EX::WARN ("No $class with id of ".$fm->id."!!");
380                 }
381         }
382
383         my %hash;
384         
385         if (ref($fm) and UNIVERSAL::isa($fm => 'Fieldmapper')) {
386                 %hash = map { defined $fm->$_ ?
387                                 ($_ => ''.$fm->$_) :
388                                 ()
389                         } grep { $_ ne $primary } $class->columns('All');
390         } else {
391                 %hash = %{$fm};
392         }
393
394         my $au = $obj->autoupdate;
395         $obj->autoupdate(0);
396         
397         #debug_object($obj);
398
399         for my $field ( keys %hash ) {
400                 $obj->$field( $hash{$field} ) if ($obj->$field ne $hash{$field});
401                 $log->debug("Setting field $field on $obj to $hash{$field}",INTERNAL);
402         }
403
404         if ($class->find_column( 'last_xact_id' ) and $obj->is_changed) {
405                 my ($xact_id) = OpenILS::Application::Storage->method_lookup('open-ils.storage.transaction.current')->run();
406                 throw Error ("Updating $class requires a transaction be established")
407                         unless ($xact_id);
408                 throw Error ("The row you are attempting to delete has been changed since you read it")
409                         unless ( $fm->last_xact_id eq $obj->last_xact_id);
410                 $obj->last_xact_id( $xact_id );
411         } else {
412                 $obj->autoupdate($au)
413         }
414
415         return $obj;
416 }
417
418
419
420         #-------------------------------------------------------------------------------
421         actor::user->has_a( home_ou => 'actor::org_unit' );
422         actor::user->has_a( card => 'actor::card' );
423         actor::user->has_a( standing => 'config::standing' );
424         actor::user->has_a( profile => 'actor::profile' );
425         actor::user->has_a( mailing_address => 'actor::user_address' );
426         actor::user->has_a( billing_address => 'actor::user_address' );
427         actor::user->has_a( ident_type => 'config::identification_type' );
428         actor::user->has_a( ident_type2 => 'config::identification_type' );
429         actor::user->has_a( net_access_level => 'config::net_access_level' );
430
431         actor::user_address->has_a( usr => 'actor::user' );
432         
433         actor::card->has_a( usr => 'actor::user' );
434         
435         actor::org_unit->has_a( parent_ou => 'actor::org_unit' );
436         actor::org_unit->has_a( ou_type => 'actor::org_unit_type' );
437         #actor::org_unit->has_a( address => 'actor::org_address' );
438
439         actor::stat_cat_entry->has_a( stat_cat => 'actor::stat_cat' );
440         actor::stat_cat->has_many( entries => 'actor::stat_cat_entry' );
441         actor::stat_cat_entry_user_map->has_a( stat_cat => 'actor::stat_cat' );
442         actor::stat_cat_entry_user_map->has_a( stat_cat_entry => 'actor::stat_cat_entry' );
443         actor::stat_cat_entry_user_map->has_a( target_usr => 'actor::user' );
444
445         asset::stat_cat_entry->has_a( stat_cat => 'asset::stat_cat' );
446         asset::stat_cat->has_many( entries => 'asset::stat_cat_entry' );
447         asset::stat_cat_entry_copy_map->has_a( stat_cat => 'asset::stat_cat' );
448         asset::stat_cat_entry_copy_map->has_a( stat_cat_entry => 'asset::stat_cat_entry' );
449         asset::stat_cat_entry_copy_map->has_a( owning_copy => 'asset::copy' );
450
451         action::survey_response->has_a( usr => 'actor::user' );
452         action::survey_response->has_a( survey => 'action::survey' );
453         action::survey_response->has_a( question => 'action::survey_question' );
454         action::survey_response->has_a( answer => 'action::survey_answer' );
455
456         action::survey_question->has_a( survey => 'action::survey' );
457
458         action::survey_answer->has_a( question => 'action::survey' );
459
460         asset::copy_note->has_a( owning_copy => 'asset::copy' );
461
462         actor::user->has_many( stat_cat_entries => [ 'actor::stat_cat_entry_user_map' => 'stat_cat_entry' ] );
463         actor::user->has_many( stat_cat_entry_user_maps => 'actor::stat_cat_entry_user_map' );
464
465         asset::copy->has_many( stat_cat_entries => [ 'asset::stat_cat_entry_copy_map' => 'stat_cat_entry' ] );
466         asset::copy->has_many( stat_cat_entry_copy_maps => 'asset::stat_cat_entry_copy_map' );
467
468         asset::copy->has_a( call_number => 'asset::call_number' );
469         asset::copy->has_a( creator => 'actor::user' );
470         asset::copy->has_a( editor => 'actor::user' );
471         asset::copy->has_a( status => 'config::copy_status' );
472         asset::copy->has_a( location => 'asset::copy_location' );
473         asset::copy->has_a( circ_lib => 'actor::org_unit' );
474
475         asset::call_number_note->has_a( call_number => 'asset::call_number' );
476
477         asset::call_number->has_a( record => 'biblio::record_entry' );
478         asset::call_number->has_a( creator => 'actor::user' );
479         asset::call_number->has_a( editor => 'actor::user' );
480
481         authority::record_note->has_a( record => 'authority::record_entry' );
482         biblio::record_note->has_a( record => 'biblio::record_entry' );
483         
484         authority::record_entry->has_a( creator => 'actor::user' );
485         authority::record_entry->has_a( editor => 'actor::user' );
486         biblio::record_entry->has_a( creator => 'actor::user' );
487         biblio::record_entry->has_a( editor => 'actor::user' );
488         
489         metabib::metarecord->has_a( master_record => 'biblio::record_entry' );
490         
491         authority::record_descriptor->has_a( record => 'authority::record_entry' );
492         metabib::record_descriptor->has_a( record => 'biblio::record_entry' );
493         
494         authority::full_rec->has_a( record => 'authority::record_entry' );
495         metabib::full_rec->has_a( record => 'biblio::record_entry' );
496         
497         metabib::title_field_entry->has_a( source => 'biblio::record_entry' );
498         metabib::title_field_entry->has_a( field => 'config::metabib_field' );
499         
500         metabib::author_field_entry->has_a( source => 'biblio::record_entry' );
501         metabib::author_field_entry->has_a( field => 'config::metabib_field' );
502         
503         metabib::subject_field_entry->has_a( source => 'biblio::record_entry' );
504         metabib::subject_field_entry->has_a( field => 'config::metabib_field' );
505         
506         metabib::keyword_field_entry->has_a( source => 'biblio::record_entry' );
507         metabib::keyword_field_entry->has_a( field => 'config::metabib_field' );
508         
509         metabib::series_field_entry->has_a( source => 'biblio::record_entry' );
510         metabib::series_field_entry->has_a( field => 'config::metabib_field' );
511         
512         metabib::metarecord_source_map->has_a( metarecord => 'metabib::metarecord' );
513         metabib::metarecord_source_map->has_a( source => 'biblio::record_entry' );
514
515         action::circulation->has_a( usr => 'actor::user' );
516         action::circulation->has_a( target_copy => 'asset::copy' );
517         action::circulation->has_a( circ_lib => 'actor::org_unit' );
518
519         money::billable_transaction->has_a( usr => 'actor::user' );
520         
521         
522         #-------------------------------------------------------------------------------
523         actor::user->has_many( survey_responses => 'action::survey_response' );
524         actor::user->has_many( addresses => 'actor::user_address' );
525         actor::user->has_many( cards => 'actor::card' );
526
527         actor::org_unit->has_many( users => 'actor::user' );
528         actor::profile->has_many( users => 'actor::user' );
529
530         action::survey->has_many( questions => 'action::survey_question' );
531         action::survey->has_many( responses => 'action::survey_response' );
532         
533         action::survey_question->has_many( answers => 'action::survey_answer' );
534         action::survey_question->has_many( responses => 'action::survey_response' );
535
536         action::survey_answer->has_many( responses => 'action::survey_response' );
537
538         asset::copy->has_many( notes => 'asset::copy_note' );
539         asset::call_number->has_many( copies => 'asset::copy' );
540         asset::call_number->has_many( notes => 'asset::call_number_note' );
541
542         authority::record_entry->has_many( record_descriptor => 'authority::record_descriptor' );
543         authority::record_entry->has_many( notes => 'authority::record_note' );
544
545         biblio::record_entry->has_many( record_descriptor => 'metabib::record_descriptor' );
546         biblio::record_entry->has_many( notes => 'biblio::record_note' );
547         biblio::record_entry->has_many( call_numbers => 'asset::call_number' );
548         biblio::record_entry->has_many( full_record_entries => 'metabib::full_rec' );
549         biblio::record_entry->has_many( title_field_entries => 'metabib::title_field_entry' );
550         biblio::record_entry->has_many( author_field_entries => 'metabib::author_field_entry' );
551         biblio::record_entry->has_many( subject_field_entries => 'metabib::subject_field_entry' );
552         biblio::record_entry->has_many( keyword_field_entries => 'metabib::keyword_field_entry' );
553         biblio::record_entry->has_many( series_field_entries => 'metabib::series_field_entry' );
554
555         metabib::metarecord->has_many( source_records => [ 'metabib::metarecord_source_map' => 'source'] );
556
557         money::billable_transaction->has_many( billings => 'money::billing' );
558         money::billable_transaction->has_many( payments => 'money::payment' );
559
560         money::grocery->has_many( billings => 'money::billing' );
561         money::grocery->has_many( payments => 'money::payment' );
562
563         money::billing->has_a( xact => 'money::billable_transaction' );
564         money::payment->has_a( xact => 'money::billable_transaction' );
565
566         money::cash_payment->has_a( xact => 'money::billable_transaction' );
567         money::cash_payment->has_a( accepting_usr => 'actor::user' );
568
569         money::check_payment->has_a( xact => 'money::billable_transaction' );
570         money::check_payment->has_a( accepting_usr => 'actor::user' );
571
572         money::credit_card_payment->has_a( xact => 'money::billable_transaction' );
573         money::credit_card_payment->has_a( accepting_usr => 'actor::user' );
574
575         money::forgive_payment->has_a( xact => 'money::billable_transaction' );
576         money::forgive_payment->has_a( accepting_usr => 'actor::user' );
577
578         money::work_payment->has_a( xact => 'money::billable_transaction' );
579         money::work_payment->has_a( accepting_usr => 'actor::user' );
580
581         money::credit_payment->has_a( xact => 'money::billable_transaction' );
582         money::credit_payment->has_a( accepting_usr => 'actor::user' );
583
584         permission::grp_tree->has_a( parent => 'permission::grp_tree' );
585
586         permission::grp_perm_map->has_a( grp => 'permission::grp_tree' );
587         permission::grp_perm_map->has_a(  perm => 'permission::perm_list' );
588         permission::grp_perm_map->has_a(  depth => 'actor::org_unit_type' );
589         
590         permission::usr_perm_map->has_a( usr => 'actor::user' );
591         permission::usr_perm_map->has_a(  perm => 'permission::perm_list' );
592         permission::usr_perm_map->has_a(  depth => 'actor::org_unit_type' );
593         
594         permission::usr_grp_map->has_a(  usr => 'actor::user' );
595         permission::usr_grp_map->has_a(  grp => 'permission::grp_tree' );
596
597         action::hold_notification->has_a(  hold => 'action::hold_request' );
598         
599         action::hold_copy_map->has_a(  hold => 'action::hold_request' );
600         action::hold_copy_map->has_a(  target_copy => 'asset::copy' );
601
602         action::hold_request->has_a(  current_copy => 'asset::copy' );
603         action::hold_request->has_a(  requestor => 'actor::user' );
604         action::hold_request->has_a(  usr => 'actor::user' );
605         action::hold_request->has_a(  pickup_lib => 'actor::org_unit' );
606         action::hold_request->has_a(  request_lib => 'actor::org_unit' );
607
608         action::hold_request->has_many(  notifications => 'action::hold_notification' );
609         action::hold_request->has_many(  eligible_copies => [ 'action::hold_copy_map' => 'target_copy' ] );
610
611         asset::copy->has_many(  holds => [ 'action::hold_copy_map' => 'hold' ] );
612
613 1;