-package SL::DB::Helpers::LinkedRecords;
+package SL::DB::Helper::LinkedRecords;
use strict;
require Exporter;
our @ISA = qw(Exporter);
-our @EXPORT = qw(linked_records link_to_record linked_records_sorted);
+our @EXPORT = qw(linked_records link_to_record);
use Carp;
use Sort::Naturally;
-use SL::DB::Helpers::Mappings;
+use SL::DB::Helper::Mappings;
use SL::DB::RecordLink;
sub linked_records {
+ my ($self, %params) = @_;
+
+ my %sort_spec = ( by => delete($params{sort_by}),
+ dir => delete($params{sort_dir}) );
+ my $filter = delete $params{filter};
+
+ my $records = _linked_records_implementation($self, %params);
+ $records = filter_linked_records($self, $filter, @{ $records }) if $filter;
+ $records = sort_linked_records($self, $sort_spec{by}, $sort_spec{dir}, @{ $records }) if $sort_spec{by};
+
+ return $records;
+}
+
+sub _linked_records_implementation {
my $self = shift;
my %params = @_;
my %from_to = ( from => delete($params{from}) || $both,
to => delete($params{to}) || $both);
- my @records = (@{ $self->linked_records(%params, direction => 'from', from => $from_to{from}) },
- @{ $self->linked_records(%params, direction => 'to', to => $from_to{to} ) });
+ my @records = (@{ _linked_records_implementation($self, %params, direction => 'from', from => $from_to{from}) },
+ @{ _linked_records_implementation($self, %params, direction => 'to', to => $from_to{to} ) });
my %record_map = map { ( ref($_) . $_->id => $_ ) } @records;
return [ values %record_map ];
}
- my $myself = $wanted eq 'from' ? 'to' : $wanted eq 'to' ? 'from' : croak("Invalid parameter `direction'");
+ if ($params{via}) {
+ croak("Cannot use 'via' without '${wanted}_table'") if !$params{$wanted};
+ croak("Cannot use 'via' with '${wanted}_table' being an array") if ref $params{$wanted};
+ }
- my $my_table = SL::DB::Helpers::Mappings::get_table_for_package(ref($self));
+ my $myself = $wanted eq 'from' ? 'to' : $wanted eq 'to' ? 'from' : croak("Invalid parameter `direction'");
+ my $my_table = SL::DB::Helper::Mappings::get_table_for_package(ref($self));
- my @query = ( "${myself}_table" => $my_table,
- "${myself}_id" => $self->id );
+ my $sub_wanted_table = "${wanted}_table";
+ my $sub_wanted_id = "${wanted}_id";
+ my ($wanted_classes, $wanted_tables);
if ($params{$wanted}) {
- my $wanted_classes = ref($params{$wanted}) eq 'ARRAY' ? $params{$wanted} : [ $params{$wanted} ];
- my $wanted_tables = [ map { SL::DB::Helpers::Mappings::get_table_for_package($_) || croak("Invalid parameter `${wanted}'") } @{ $wanted_classes } ];
- push @query, ("${wanted}_table" => $wanted_tables);
+ $wanted_classes = ref($params{$wanted}) eq 'ARRAY' ? $params{$wanted} : [ $params{$wanted} ];
+ $wanted_tables = [ map { SL::DB::Helper::Mappings::get_table_for_package($_) || croak("Invalid parameter `${wanted}'") } @{ $wanted_classes } ];
}
- my $links = SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]);
+ my @get_objects_query = ref($params{query}) eq 'ARRAY' ? @{ $params{query} } : ();
+ my $get_objects = sub {
+ my ($link) = @_;
+ my $manager_class = SL::DB::Helper::Mappings::get_manager_package_for_table($link->$sub_wanted_table);
+ my $object_class = SL::DB::Helper::Mappings::get_package_for_table($link->$sub_wanted_table);
+ eval "require " . $object_class . "; 1;";
+ return map {
+ $_->{_record_link_direction} = $wanted;
+ $_->{_record_link} = $link;
+ $_
+ } @{ $manager_class->get_all(query => [ id => $link->$sub_wanted_id, @get_objects_query ]) };
+ };
- my $sub_wanted_table = "${wanted}_table";
- my $sub_wanted_id = "${wanted}_id";
+ # If no 'via' is given then use a simple(r) method for querying the wanted objects.
+ if (!$params{via}) {
+ my @query = ( "${myself}_table" => $my_table,
+ "${myself}_id" => $self->id );
+ push @query, ( "${wanted}_table" => $wanted_tables ) if $wanted_tables;
- my $records = [];
- @query = ref($params{query}) eq 'ARRAY' ? @{ $params{query} } : ();
+ return [ map { $get_objects->($_) } @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) } ];
+ }
- foreach my $link (@{ $links }) {
- my $manager_class = SL::DB::Helpers::Mappings::get_manager_package_for_table($link->$sub_wanted_table);
- my $object_class = SL::DB::Helpers::Mappings::get_package_for_table($link->$sub_wanted_table);
- eval "require " . $object_class . "; 1;";
- push @{ $records }, @{ $manager_class->get_all(query => [ id => $link->$sub_wanted_id, @query ]) };
+ # More complex handling for the 'via' case.
+ my @sources = ( $self );
+ my @targets = map { SL::DB::Helper::Mappings::get_table_for_package($_) } @{ ref($params{via}) ? $params{via} : [ $params{via} ] };
+ push @targets, @{ $wanted_tables } if $wanted_tables;
+
+ my %seen = map { ($_->meta->table . $_->id => 1) } @sources;
+
+ while (@targets) {
+ my @new_sources = @sources;
+ foreach my $src (@sources) {
+ my @query = ( "${myself}_table" => $src->meta->table,
+ "${myself}_id" => $src->id,
+ "${wanted}_table" => \@targets );
+ push @new_sources,
+ map { $get_objects->($_) }
+ grep { !$seen{$_->$sub_wanted_table . $_->$sub_wanted_id} }
+ @{ SL::DB::Manager::RecordLink->get_all(query => [ and => \@query ]) };
+ }
+
+ @sources = @new_sources;
+ %seen = map { ($_->meta->table . $_->id => 1) } @sources;
+ shift @targets;
}
- return $records;
+ my %wanted_tables_map = map { ($_ => 1) } @{ $wanted_tables };
+ return [ grep { $wanted_tables_map{$_->meta->table} } @sources ];
}
sub link_to_record {
my $self = shift;
my $other = shift;
+ my %params = @_;
croak "self has no id" unless $self->id;
croak "other has no id" unless $other->id;
- my %params = ( from_table => SL::DB::Helpers::Mappings::get_table_for_package(ref($self)),
- from_id => $self->id,
- to_table => SL::DB::Helpers::Mappings::get_table_for_package(ref($other)),
- to_id => $other->id,
- );
+ my @directions = ([ 'from', 'to' ]);
+ push @directions, [ 'to', 'from' ] if $params{bidirectional};
+ my @links;
- my $link = SL::DB::Manager::RecordLink->find_by(and => [ %params ]);
- return $link ? $link : SL::DB::RecordLink->new(%params)->save;
-}
+ foreach my $direction (@directions) {
+ my %data = ( $direction->[0] . "_table" => SL::DB::Helper::Mappings::get_table_for_package(ref($self)),
+ $direction->[0] . "_id" => $self->id,
+ $direction->[1] . "_table" => SL::DB::Helper::Mappings::get_table_for_package(ref($other)),
+ $direction->[1] . "_id" => $other->id,
+ );
-sub linked_records_sorted {
- my ($self, $sort_by, $sort_dir, %params) = @_;
+ my $link = SL::DB::Manager::RecordLink->find_by(and => [ %data ]);
+ push @links, $link ? $link : SL::DB::RecordLink->new(%data)->save unless $link;
+ }
- return sort_linked_records($self, $sort_by, $sort_dir, $self->linked_records(%params));
+ return wantarray ? @links : $links[0];
}
sub sort_linked_records {
return [ sort($comparator @records) ];
}
+sub filter_linked_records {
+ my ($self_or_class, $filter, @records) = @_;
+
+ if ($filter eq 'accessible') {
+ my $employee = SL::DB::Manager::Employee->current;
+ @records = grep { !$_->can('may_be_accessed') || $_->may_be_accessed($employee) } @records;
+ } else {
+ croak "Unsupported filter parameter '${filter}'";
+ }
+
+ return \@records;
+}
+
1;
__END__
=head1 NAME
-SL::DB::Helpers::LinkedRecords - Mixin for retrieving linked records via the table C<record_links>
+SL::DB::Helper::LinkedRecords - Mixin for retrieving linked records via the table C<record_links>
=head1 FUNCTIONS
all records linked from or to C<$self> are returned.
The optional parameter C<from> or C<to> (same as C<direction>)
-contains the package names of Rose models for table limitation. It can
-be a single model name as a single scalar or multiple model names in
-an array reference in which case all links matching any of the model
-names will be returned.
+contains the package names of Rose models for table limitation (the
+prefix C<SL::DB::> is optional). It can be a single model name as a
+single scalar or multiple model names in an array reference in which
+case all links matching any of the model names will be returned.
+
+The optional parameter C<via> can be used to retrieve all documents
+that may have intermediate documents inbetween. It is an array
+reference of Rose package names for the models that may be
+intermediate link targets. One example is retrieving all invoices for
+a given quotation no matter whether or not orders and delivery orders
+have been created. If C<via> is given then C<from> or C<to> (depending
+on C<direction>) must be given as well, and it must then not be an
+array reference.
+
+Examples:
-If you only need invoices created from an order C<$order> then the
-call could look like this:
+If you only need invoices created directly from an order C<$order> (no
+delivery orders inbetween) then the call could look like this:
my $invoices = $order->linked_records(direction => 'to',
- to => 'SL::DB::Invoice');
+ to => 'Invoice');
+
+Retrieving all invoices from a quotation no matter whether or not
+orders or delivery orders where created:
+
+ my $invoices = $quotation->linked_records(direction => 'to',
+ to => 'Invoice',
+ via => [ 'Order', 'DeliveryOrder' ]);
The optional parameter C<query> can be used to limit the records
returned. The following call limits the earlier example to invoices
created today:
my $invoices = $order->linked_records(direction => 'to',
- to => 'SL::DB::Invoice',
+ to => 'Invoice',
query => [ transdate => DateTime->today_local ]);
-Returns an array reference.
+The optional parameters C<$params{sort_by}> and C<$params{sort_dir}>
+can be used in order to sort the result. If C<$params{sort_by}> is
+trueish then the result is sorted by calling L</sort_linked_records>.
+
+The optional parameter C<$params{filter}> controls whether or not the
+result is filtered. Supported values are:
+
+=over 2
+
+=item C<accessible>
+
+Removes all objects for which the function C<may_be_accessed> from the
+mixin L<SL::DB::Helper::MayBeAccessed> exists and returns falsish for
+the current employee.
+
+=back
+
+Returns an array reference. Each element returned is a Rose::DB
+instance. Additionally several elements in the element returned are
+set to special values:
+
+=over 2
+
+=item C<_record_link_direction>
+
+Either C<from> or C<to> indicating the direction. C<from> means that
+this object is the source in the link.
+
+=item C<_record_link>
-=item C<link_to_record $record>
+The actual database link object (an instance of L<SL::DB::RecordLink>).
+
+=back
+
+=item C<link_to_record $record, %params>
Will create an entry in the table C<record_links> with the C<from>
side being C<$self> and the C<to> side being C<$record>. Will only
insert a new entry if such a link does not already exist.
-Returns either the existing link or the newly created one as an
-instance of C<SL::DB::RecordLink>.
+If C<$params{bidirectional}> is trueish then another link will be
+created with the roles of C<from> and C<to> reversed. This link will
+also only be created if it doesn't exist already.
+
+In scalar contenxt returns either the existing link or the newly
+created one as an instance of C<SL::DB::RecordLink>. In array context
+it returns an array of links (one entry if C<$params{bidirectional}>
+is falsish and two entries if it is trueish).
=item C<sort_linked_records $sort_by, $sort_dir, @records>
This function is not exported.
-=item C<linked_records_sorted $sort_by, $sort_dir, %params>
-
-Returns the result of L</linked_records> sorted by
-L</sort_linked_records>. C<%params> is passed to
-L</linked_records>. C<$sort_by> and C<$sort_dir> are passed to
-L</sort_linked_records>.
-
=back
=head1 EXPORTS
-This mixin exports the functions L</linked_records>,
-L</link_to_record> and L</linked_records_sorted>.
+This mixin exports the functions L</linked_records> and
+L</link_to_record>.
=head1 BUGS