package Bric::Biz::Workflow; ############################################################################### =head1 NAME Bric::Biz::Workflow - Controls the progress of an asset through a series of desks. =head1 VERSION $LastChangedRevision$ =cut require Bric; our $VERSION = Bric->VERSION; =head1 DATE $LastChangedDate: 2005-08-19 20:54:48 +0200 (Fri, 19 Aug 2005) $ =head1 SYNOPSIS my $flow = new Bric::Biz::Workflow($param); $id = $flow->get_id; $name = $flow->get_name; $flow = $flow->set_name($name); $site_id = $flow->get_site_id; $flow = $flow->set_site_id($site_id); $desc = $flow->get_description; $flow = $flow->set_description($desc); $flow = $flow->add_desk($param); # Returns a list of allowed desks. @desks = $flow->allowed_desks(); # Lists the required desks @desks = $flow->required_desks(); =head1 DESCRIPTION A workflow is something that guides an asset through a set of desks, where an asset is any kind of creative content (a story, an image, a sound file, etc) and a desk performs and kind of validation or transformation upon an asset needed before it can be published. Example desks might be a 'legal' desk where users can verify any legal issue for a particular asset, or an 'edit' desk where users can check consistancy and presentation for an asset. A workflow might be as simple as a linear path through a set of desks or as complex as requiring certain desks be visited with other desks optional and a route through the desks that can be arbitrarily complex. =cut #==============================================================================# # Dependencies # #======================================# #--------------------------------------# # Standard Dependencies use strict; #--------------------------------------# # Programatic Dependencies use Bric::Util::DBI qw(:all); use Bric::Util::Grp::Desk; use Bric::Util::Grp::Workflow; use Bric::Util::Fault qw(throw_dp throw_ap); use Bric::Biz::Workflow::Parts::Desk; use Bric::Biz::Site; #==============================================================================# # Inheritance # #======================================# use base qw( Bric Exporter ); our @EXPORT_OK = qw(TEMPLATE_WORKFLOW STORY_WORKFLOW MEDIA_WORKFLOW WORKFLOW_TYPE_MAP ); our %EXPORT_TAGS = (wf_const => [qw(TEMPLATE_WORKFLOW STORY_WORKFLOW MEDIA_WORKFLOW WORKFLOW_TYPE_MAP)], ); #=============================================================================# # Function Prototypes # #======================================# my $get_em; #==============================================================================# # Constants # #======================================# use constant DEBUG => 1; use constant HAS_MULTISITE => 1; use constant ASSET_GRP_PKG => 'Bric::Util::Grp::Asset'; use constant DESK_PKG => 'Bric::Biz::Workflow::Parts::Desk'; use constant GROUP_PACKAGE => 'Bric::Util::Grp::Workflow'; use constant INSTANCE_GROUP_ID => 25; use constant TEMPLATE_WORKFLOW => 1; use constant STORY_WORKFLOW => 2; use constant MEDIA_WORKFLOW => 3; use constant WORKFLOW_TYPE_MAP => { &STORY_WORKFLOW => 'Story', &MEDIA_WORKFLOW => 'Media', &TEMPLATE_WORKFLOW => 'Template' }; #==============================================================================# # Fields # #======================================# #--------------------------------------# # Public Class Fields # None. #--------------------------------------# # Private Class Fields my $meths; my $table = 'workflow'; my @cols = qw(name description asset_grp_id all_desk_grp_id head_desk_id req_desk_grp_id type active site__id); my @props = qw(name description asset_grp_id all_desk_grp_id head_desk_id req_desk_grp_id type _active site_id); my $sel_cols = 'a.id, a.name, a.description, a.asset_grp_id, ' . 'a.all_desk_grp_id, a.head_desk_id, a.req_desk_grp_id, a.type, a.active, ' . 'a.site__id, m.grp__id'; my @sel_props = ('id', @props, 'grp_ids'); my @ord = qw(name description type active site_id site); #--------------------------------------# # Instance Fields BEGIN { Bric::register_fields({ # Public Fields 'id' => Bric::FIELD_READ, 'name' => Bric::FIELD_RDWR, 'description' => Bric::FIELD_RDWR, 'asset_grp_id' => Bric::FIELD_READ, 'all_desk_grp_id' => Bric::FIELD_READ, 'req_desk_grp_id' => Bric::FIELD_READ, 'head_desk_id' => Bric::FIELD_READ, 'type' => Bric::FIELD_RDWR, 'grp_ids' => Bric::FIELD_READ, #Which site is this Workflow associated with 'site_id' => Bric::FIELD_RDWR, 'site' => Bric::FIELD_READ , # Private Fields '_all_desk_grp_obj' => Bric::FIELD_NONE, '_req_desk_grp_obj' => Bric::FIELD_NONE, '_allowed_desks' => Bric::FIELD_NONE, '_head_desk_obj' => Bric::FIELD_NONE, '_active' => Bric::FIELD_NONE, '_remove' => Bric::FIELD_NONE, }); } #==============================================================================# =head1 INTERFACE =head2 Constructors =over 4 =item $success = $obj = new Bric::Biz::Workflow($param); Keys for $param are: =over 4 =item * name The name for this workflow =item * site_id The site this workflow belongs to =item * description A description for this workflow =item * start_desk The starting desk for this workflow =back B NONE B NONE B NONE =cut sub new { my ($self, $init) = @_; my $sd = delete $init->{start_desk}; $init->{_active} = 1; push @{$init->{grp_ids}}, INSTANCE_GROUP_ID; # Call the parent's constructor. $self = $self->SUPER::new($init); # Add the start desk if passed. $self->set_start_desk($sd) if $sd; # Since this is a new object, set the dirty bit so it will be saved. $self->_set__dirty(1); # Return the object. return $self; } #------------------------------------------------------------------------------# =item my $wf = Bric::Biz::Workflow->lookup({ id => $id }); =item my $wf = Bric::Biz::Workflow->lookup({ name => $name }); Looks up and instantiates a new Bric::Biz::Workflow object based on an Bric::Biz::Workflow object ID or name. If no output channelobject is found in the database, C returns C. B =over 4 =item * Unable to prepare SQL statement. =item * Unable to connect to database. =item * Unable to select column into arrayref. =item * Unable to execute SQL statement. =item * Unable to bind to columns to statement handle. =item * Unable to fetch row from statement handle. =back B NONE. B NONE. =cut sub lookup { my $pkg = shift; my $wf = $pkg->cache_lookup(@_); return $wf if $wf; $wf = $get_em->($pkg, @_); # We want @$wf to have only one value. throw_dp(error => 'Too many ' . __PACKAGE__ . ' objects found.') if @$wf > 1; return @$wf ? $wf->[0] : undef; } #------------------------------------------------------------------------------# =item (@all || $all) = Bric::Biz::Workflow->list($params); Return a list of all known workflow types. Keys of the $params hash reference are: =over 4 =item C Workflow ID. May use C for a list of possible values. =item C Return all workflows matching a certain name. May use C for a list of possible values. =item C Return all workflows matching a certain site id. May use C for a list of possible values. =item C Return all workflows with a matching description. May use C for a list of possible values. =item C Return all workflows with a matching description. May use C for a list of possible values. =item C Boolean; Return all in/active workflows. =item C Return all workflows of a particular type. The types are integers accessible via the C, C, and C constants. May use C for a list of possible values. =item Return all workflows containing a desk with this desk ID. May use C for a list of possible values. =item C Return all workflows in the group corresponding to this group ID. May use C for a list of possible values. =back B =over 4 =item * Unable to prepare SQL statement. =item * Unable to connect to database. =item * Unable to select column into arrayref. =item * Unable to execute SQL statement. =item * Unable to bind to columns to statement handle. =item * Unable to fetch row from statement handle. =back B NONE. B Seaches against C and C use the LIKE operator, so '%' can be used for substring searching. =cut sub list { wantarray ? @{ &$get_em(@_) } : &$get_em(@_) } #------------------------------------------------------------------------------# =item (@ids || $ids) = Bric::Biz::Workflow->list_ids($params); Return a list of workflow IDs. See C for a list of the relevant keys in the C<$params> hash reference. B =over 4 =item * Unable to prepare SQL statement. =item * Unable to connect to database. =item * Unable to select column into arrayref. =item * Unable to execute SQL statement. =item * Unable to bind to columns to statement handle. =item * Unable to fetch row from statement handle. =back B NONE. B Seaches against C and C use the LIKE operator, so '%' can be used for substring searching. =cut sub list_ids { wantarray ? @{ &$get_em(@_, 1) } : &$get_em(@_, 1) } #--------------------------------------# =back =head2 Destructors =over 4 =item $self->DESTROY Dummy method to prevent wasting time trying to AUTOLOAD DESTROY. =cut sub DESTROY { # This method should be here even if its empty so that we don't waste time # making Bricolage's autoload method try to find it. } #--------------------------------------# =back =head2 Public Class Methods =over 4 =item my $meths = Bric::Biz::Workflow->my_meths =item my (@meths || $meths_aref) = Bric::Biz::Workflow->my_meths(TRUE) =item my (@meths || $meths_aref) = Bric::Biz::Workflow->my_meths(0, TRUE) Returns an anonymous hash of introspection data for this object. If called with a true argument, it will return an ordered list or anonymous array of introspection data. If a second true argument is passed instead of a first, then a list or anonymous array of introspection data will be returned for properties that uniquely identify an object (excluding C, which is assumed). Each hash key is the name of a property or attribute of the object. The value for a hash key is another anonymous hash containing the following keys: =over 4 =item name The name of the property or attribute. Is the same as the hash key when an anonymous hash is returned. =item disp The display name of the property or attribute. =item get_meth A reference to the method that will retrieve the value of the property or attribute. =item get_args An anonymous array of arguments to pass to a call to get_meth in order to retrieve the value of the property or attribute. =item set_meth A reference to the method that will set the value of the property or attribute. =item set_args An anonymous array of arguments to pass to a call to set_meth in order to set the value of the property or attribute. =item type The type of value the property or attribute contains. There are only three types: =over 4 =item short =item date =item blob =back =item len If the value is a 'short' value, this hash key contains the length of the field. =item search The property is searchable via the list() and list_ids() methods. =item req The property or attribute is required. =item props An anonymous hash of properties used to display the property or attribute. Possible keys include: =over 4 =item type The display field type. Possible values are =over 4 =item text =item textarea =item password =item hidden =item radio =item checkbox =item select =back =item length The Length, in letters, to display a text or password field. =item maxlength The maximum length of the property or value - usually defined by the SQL DDL. =back =item rows The number of rows to format in a textarea field. =item cols The number of columns to format in a textarea field. =item vals An anonymous hash of key/value pairs reprsenting the values and display names to use in a select list. =back B NONE. B NONE. B NONE. =cut sub my_meths { my ($pkg, $ord, $ident) = @_; # Create 'em if we haven't got 'em. $meths ||= { name => { name => 'name', get_meth => sub { shift->get_name(@_) }, get_args => [], set_meth => sub { shift->set_name(@_) }, set_args => [], disp => 'Name', type => 'short', len => 64, req => 1, search => 1, props => { type => 'text', length => 32, maxlength => 64 } }, description => { get_meth => sub { shift->get_description(@_) }, get_args => [], set_meth => sub { shift->set_description(@_) }, set_args => [], name => 'description', disp => 'Description', len => 256, req => 0, type => 'short', props => { type => 'textarea', cols => 40, rows => 4 } }, type => { get_meth => sub { shift->get_type(@_) }, get_args => [], set_meth => sub { shift->set_type(@_) }, set_args => [], name => 'type', disp => 'Type', len => 1, req => 1, type => 'short', props => { type => 'select', vals => [ [STORY_WORKFLOW, 'Story'], [MEDIA_WORKFLOW, 'Media'], [TEMPLATE_WORKFLOW, 'Template'] ], } }, site_id => { get_meth => sub { shift->get_site_id(@_) }, get_args => [], set_meth => sub { shift->set_site_id(@_) }, set_args => [], name => 'site_id', disp => 'Site ID', len => 10, req => 1, type => 'short', props => {} }, site => { name => 'site', get_meth => sub { my $s = Bric::Biz::Site->lookup ({ id => shift->get_site_id }) or return; $s->get_name; }, disp => 'Site', type => 'short', req => 0, props => { type => 'text', length => 10, maxlength => 10 } }, active => { name => 'active', get_meth => sub { shift->is_active(@_) ? 1 : 0 }, get_args => [], set_meth => sub { $_[1] ? shift->activate(@_) : shift->deactivate(@_) }, set_args => [], disp => 'Active', len => 1, req => 1, type => 'short', props => { type => 'checkbox' } } }; if ($ord) { return wantarray ? @{$meths}{@ord} : [@{$meths}{@ord}]; } elsif ($ident) { return; } else { return $meths; } } #--------------------------------------# =back =head2 Public Instance Methods =over 4 =item $id = $workflow->get_id Returns the workflow's ID. B NONE. B NONE. B NONE. =item $site_id = $workflow->get_site_id Returns the ID of the site this Workflow is a part of B NONE. B NONE. B NONE. =item $workflow = $workflow->set_site_id($site_id) Set the ID of the site this Workflow should be a part of B NONE. B NONE. B NONE. =item $flow->add_desk($param); Add a desk to this workflow. Keys to param are: =over 4 =item * allowed Add a list of desks as part of the allowed desks. =item * required Add a list of desks as required desks. =back B NONE B NONE B NONE =cut sub add_desk { my $self = shift; my ($param) = @_; my $all_grp = $self->_get_all_desk_grp; my $req_grp = $self->_get_req_desk_grp; my (@all, @req); push @all, @{$param->{'allowed'}} if $param->{'allowed'}; if ($param->{'required'}) { push @all, @{$param->{'required'}}; push @req, @{$param->{'required'}}; } # Add all the desks to the desk group. $all_grp->add_members([map {ref $_ ? {'obj' => $_} : {'id' => $_, 'package' => DESK_PKG} } @all]); $req_grp->add_members([map {ref $_ ? {'obj' => $_} : {'id' => $_, 'package' => DESK_PKG} } @req]); return $self->_set(['_allowed_desks'] => []); } #------------------------------------------------------------------------------# =item $flow->del_desk([$desk || $desk_id]); Delete a desk from this workflow. B NONE B NONE B NONE =cut sub del_desk { my $self = shift; my ($desks) = @_; my $all_grp = $self->_get_all_desk_grp; my $req_grp = $self->_get_req_desk_grp; my $vals; foreach my $d (@$desks) { my $id; if (ref $d) { $id = $d->get_id; push @$vals, { obj => $d }; } else { $id = $d; push @$vals, { package => DESK_PKG, id => $d }; } # Clear out the head desk stuff if they delete the head desk. if ($self->get_head_desk_id == $id) { $self->_set(['head_desk_id', '_head_desk_obj'], [undef, undef]); } } # Delete the desks from the desk groups. $all_grp->delete_members($vals); $req_grp->delete_members($vals); return $self->_set(['_allowed_desks'] => []); } #------------------------------------------------------------------------------# =item $flow->allowed_desks(); Returns a list of allowed desks. B NONE B NONE B NONE =cut sub allowed_desks { my $self = shift; my $desks = $self->_get('_allowed_desks'); unless ($desks) { my $all_grp = $self->_get_all_desk_grp or return; # Sort desks so that the start desk is first, normal desks come next # and the publish desk is last. $desks = [ map { $_->{obj} } sort { $b->{start} <=> $a->{start} || $a->{pub} <=> $b->{pub} || $a->{id} <=> $b->{id} } map {{ obj => $_, id => $_->get_id, pub => ($_->can_publish || 0), start => ($self->is_start_desk($_) || 0) }} grep { $_->is_active } $all_grp->get_objects ]; $self->_set(['_allowed_desks'] => [$desks]); } return wantarray ? @$desks : $desks; } #------------------------------------------------------------------------------# =item $bool = $flow->desk_in_allowed($desk_obj); Returns true if desk is in the list of allowed desks. B NONE B NONE B NONE =cut sub desk_in_allowed { my $self = shift; my ($desk_obj) = @_; my $all_grp = $self->_get_all_desk_grp; return unless $all_grp; return $all_grp->has_member({ obj => $desk_obj }); } #------------------------------------------------------------------------------# =item @desks = $flow->required_desks(); Lists the required desks B NONE B NONE B NONE =cut sub required_desks { my $self = shift; my $req_grp = $self->_get_req_desk_grp; return unless $req_grp; my @mem = sort { $a->get_id <=> $b->get_id } $req_grp->get_members; # Drop any inactive desks from the list. @mem = grep($_->is_active, @mem); return wantarray ? @mem : \@mem; } #------------------------------------------------------------------------------# =item $bool = $flow->desk_in_required($desk_obj); Returns true if the deskref given is in the required list B NONE B NONE B NONE =cut sub desk_in_required { my $self = shift; my ($desk_obj) = @_; my $req_grp = $self->_get_req_desk_grp; return unless $req_grp; return $req_grp->has_member({ obj => $desk_obj }); } #------------------------------------------------------------------------------# =item $desk = $flow->get_start_desk(); =item ($flow || undef) = $flow->set_start_desk($desk_id); =item $self = $flow->is_start_desk(); Get/Set the start desk. B NONE B NONE B =cut sub get_start_desk { my $self = shift; my ($id, $head) = $self->_get('head_desk_id', '_head_desk_obj'); return $head if $head; return unless $id; $head = Bric::Biz::Workflow::Parts::Desk->lookup({'id' => $id}); return unless $head; $self->_set(['_head_desk_obj'], [$head]); return $head; } sub set_start_desk { my $self = shift; my ($val) = @_; # Grab an ID if they pass a desk object. my ($id, $desk) = ref $val ? ($val->get_id, $val) : ($val); # Add desk to the required list. Should do nothing if its already there. $self->add_desk({ required => [$id] }); $self->_set([qw(head_desk_id _head_desk_obj)], [$id, $desk]) } sub is_start_desk { my $self = shift; my ($d) = @_; my ($id, $head) = $self->_get('head_desk_id', '_head_desk_obj'); # Return if they didn't pass a desk; return unless $d; # Return our self object if it matches the start desk ID. return $self if $d->get_id eq $id; # Return undef if its not the start desk; separate to prevent array context return; } #------------------------------------------------------------------------------# =item $desk || undef = $desk->is_active; =item $desk = $desk->activate; =item $desk = $desk->deactivate; Get/Set the active flag. B NONE B NONE B NONE =cut sub is_active { my $self = shift; return $self->_get('_active') ? $self : undef; } sub activate { my $self = shift; $self->_set__dirty(1); $self->_set(['_active'], [1]) and return $self; } sub deactivate { my $self = shift; $self->_set__dirty(1); $self->_set(['_active'], [0]) and return $self; } #------------------------------------------------------------------------------# =item $desk->remove; Get/Set the active flag. B NONE B NONE B This method is deprecated. =cut sub remove { my $self = shift; $self->_set__dirty; $self->_set(['_remove'], [1]); my ($pkg, $file, $line) = caller; warn __PACKAGE__ . "->remove has been deprecated"; } #------------------------------------------------------------------------------# =item $save = $workflow->save; Save this workflow B NONE B NONE B NONE =cut sub save { my $self = shift; my $all_grp = $self->_get_all_desk_grp; my $req_grp = $self->_get_req_desk_grp; my $id = $self->get_id; # Make sure they don't try to save with out setting a start desk. unless ($self->get_start_desk) { my $err_msg = 'No start desk: A start desk must be defined using '. "'set_start_desk' before 'save' is called"; throw_ap(error => $err_msg); } unless ($self->_get('_remove')) { $all_grp->save if $all_grp; $req_grp->save if $req_grp; # Set the ID if the objects were saved. $self->_set(['all_desk_grp_id'], [$all_grp->get_id]) if $all_grp; $self->_set(['req_desk_grp_id'], [$req_grp->get_id]) if $req_grp; # Only update if anything has changed. return unless $self->_get__dirty; if ($id) { $self->_update_workflow; } else { $self->_insert_workflow; } $self->SUPER::save(); } else { $all_grp->deactivate and $all_grp->save if $all_grp; $req_grp->deactivate and $req_grp->save if $req_grp; $self->_remove_workflow; } return $self; } #==============================================================================# =back =head1 Private Methods NONE. =head2 Private Class Methods NONE. =head2 Private Instance Methods =over 4 =cut # We need to documente these! sub _get_all_desk_grp { my $self = shift; my ($id, $grp) = $self->_get('all_desk_grp_id', '_all_desk_grp_obj'); # Return the group if we have it return $grp if $grp; if ($id) { $grp = Bric::Util::Grp::Desk->lookup({'id' => $id}); } else { my $desc = 'All desks available to a workflow'; $grp = Bric::Util::Grp::Desk->new({'name' => 'All Workflow Desks', 'description' => $desc}); } $self->_set(['_all_desk_grp_obj'], [$grp]); return $grp; } sub _get_req_desk_grp { my $self = shift; my ($id, $grp) = $self->_get('req_desk_grp_id', '_req_desk_grp_obj'); # Return the group if we have it return $grp if $grp; if ($id) { $grp = Bric::Util::Grp::Desk->lookup({'id' => $id}); } else { my $desc = 'Desks required in a workflow'; $grp = Bric::Util::Grp::Desk->new({'name' => 'Required Workflow Desks', 'description' => $desc}); } $self->_set(['_req_desk_grp_obj'], [$grp]); return $grp; } sub _insert_workflow { my $self = shift; # Create a new asset group. my $grp = Bric::Util::Grp::Asset->new ({ name => 'Workflow Assets', description => 'Assets group for workflow permissions' }); $grp->save; $self->_set(['asset_grp_id'], [$grp->get_id]); # Create the insert statement. my $nextval = next_key($table); my $ins = prepare_c(qq{ INSERT INTO $table (id, ${\join(', ', @cols)}) VALUES ($nextval, ${\join(', ', ('?') x @cols)}) }, undef); execute($ins, $self->_get(@props)); # Set the ID of this object. $self->_set(['id'],[last_key($table)]); # And finally, register this workflow in the "All Workflows" group. $self->register_instance(INSTANCE_GROUP_ID, GROUP_PACKAGE); return $self; } sub _update_workflow { my $self = shift; my $sql = "UPDATE $table SET " . join(',', map { "$_ = ?" } @cols) . " WHERE id = ?"; my $sth = prepare_c($sql, undef); execute($sth, $self->_get(@props), $self->get_id); return 1; } #------------------------------------------------------------------------------# =item $desk = $desk->_remove_workflow Remove this workflow B NONE B NONE B NONE =cut sub _remove_workflow { my $self = shift; my $sth = prepare_c("DELETE FROM $table WHERE id = ?", undef); execute($sth, $self->get_id); return $self; } =back =head2 Private Functions =over 4 =item my $wf_aref = &$get_em( $pkg, $search_href ) =item my $wf_ids_aref = &$get_em( $pkg, $search_href, 1 ) Function used by C and C to return a list of Bric::Biz::Workflow objects or, if called with an optional third argument, returns a list of Bric::Biz::Workflow object IDs (used by C). B =over 4 =item * Unable to prepare SQL statement. =item * Unable to connect to database. =item * Unable to select column into arrayref. =item * Unable to execute SQL statement. =item * Unable to bind to columns to statement handle. =item * Unable to fetch row from statement handle. =back B NONE. B NONE. =cut $get_em = sub { my ($pkg, $params, $ids) = @_; # Make sure to set active explictly if its not passed. $params->{active} = exists $params->{active} ? $params->{active} ? 1 : 0 : 1 unless $params->{id}; my $tables = "$table a, member m, workflow_member c"; my $wheres = 'a.id = c.object_id AND c.member__id = m.id AND ' . "m.active = '1'"; my @params; while (my ($k, $v) = each %$params) { if ($k eq 'name' or $k eq 'description') { $wheres .= ' AND ' . any_where $v, "LOWER(a.$k) LIKE LOWER(?)", \@params; } elsif ($k eq 'grp_id') { $tables .= ", member m2, workflow_member c2"; $wheres .= " AND a.id = c2.object_id AND c2.member__id = m2.id" . " AND m2.active = '1' AND " . any_where $v, 'm2.grp__id = ?', \@params; } elsif ($k eq 'desk_id') { # Yes, this is a hack. It requires too much knowledge of the Group # schema. This will go away once Workflow has this group stuff # refactored out of it. $tables .= ", member m3, desk_member c3"; $wheres .= ' AND a.all_desk_grp_id = m3.grp__id AND ' . "m3.id = c3.member__id AND m3.active = '1' AND " . any_where $v, 'c3.object_id = ?', \@params; } elsif ($k eq 'site_id') { $wheres .= ' AND ' . any_where $v, 'a.site__id = ?', \@params; } elsif ($k eq 'active') { # Simple lookup by "active" boolean. $wheres .= ' AND a.active = ?'; push @params, $v ? 1 : 0; } else { $wheres .= ' AND ' . any_where $v, "a.$k = ?", \@params; } } my ($qry_cols, $order) = $ids ? (\'DISTINCT a.id', 'a.id') : (\$sel_cols, 'a.name, a.id'); my $sel = prepare_c(qq{ SELECT $$qry_cols FROM $tables WHERE $wheres ORDER BY $order }, undef); # Just return the IDs, if they're what's wanted. return col_aref($sel, @params) if $ids; execute($sel, @params); my (@d, @wfs, $grp_ids); bind_columns($sel, \@d[0..$#sel_props]); my $last = -1; $pkg = ref $pkg || $pkg; while (fetch($sel)) { if ($d[0] != $last) { $last = $d[0]; # Create a new workflow object. my $self = bless {}, $pkg; $self->SUPER::new; # Get a reference to the array of group IDs. $grp_ids = $d[$#d] = [$d[$#d]]; $self->_set(\@sel_props, \@d); $self->_set__dirty; # Disables dirty flag. push @wfs, $self->cache_me; } else { push @$grp_ids, $d[$#d]; } } return \@wfs; }; 1; __END__ =back =head1 NOTES NONE =head1 AUTHOR Garth Webb =head1 SEE ALSO L, L, L =cut