package Bric::SOAP::Workflow; ############################################################################### use strict; use warnings; use Bric::Biz::Asset::Business::Story; use Bric::Biz::Asset::Business::Media; use Bric::Biz::Asset::Template; use Bric::Biz::OutputChannel; use Bric::Biz::Site; use Bric::Biz::Workflow qw(:wf_const); use Bric::App::Cache; use Bric::App::Session qw(get_user_id); use Bric::App::Authz qw(chk_authz READ EDIT CREATE); use Bric::Config qw(STAGE_ROOT PREVIEW_ROOT PREVIEW_LOCAL ISO_8601_FORMAT); use Bric::App::Event qw(log_event); use Bric::Util::Time qw(strfdate local_date); use Bric::Util::MediaType; use Bric::Util::Fault qw(throw_ap); use Bric::Util::Priv::Parts::Const qw(:all); use Bric::Util::Job::Pub; use Bric::Dist::ServerType; use Bric::Dist::Resource; use Bric::Biz::Workflow::Parts::Desk; use Bric::SOAP::Util qw(xs_date_to_db_date parse_asset_document); use SOAP::Lite; import SOAP::Data 'name'; use base qw(Bric::SOAP::Asset); use constant DEBUG => 0; require Data::Dumper if DEBUG; # We'll use this for outputting messages. my %types = ( story => 'Story', media => 'Media', template => 'Template', ); # We'll use this for finding workflows. my %wf_types = ( story => STORY_WORKFLOW, media => MEDIA_WORKFLOW, template => TEMPLATE_WORKFLOW, ); =head1 NAME Bric::SOAP::Workflow - SOAP interface to Bricolage workflows. =head1 VERSION $LastChangedRevision$ =cut require Bric; our $VERSION = Bric->VERSION; =head1 DATE $LastChangedDate: 2006-03-18 01:26:34 +0100 (Sat, 18 Mar 2006) $ =head1 SYNOPSIS use SOAP::Lite; import SOAP::Data 'name'; # setup soap object to login with my $soap = new SOAP::Lite uri => 'http://bricolage.sourceforge.net/Bric/SOAP/Auth', readable => DEBUG; $soap->proxy('http://localhost/soap', cookie_jar => HTTP::Cookies->new(ignore_discard => 1)); # login $soap->login(name(username => USER), name(password => PASSWORD)); # set uri for Workflow module $soap->uri('http://bricolage.sourceforge.net/Bric/SOAP/Workflow'); =head1 DESCRIPTION This module provides a SOAP interface to manipulating Bricolage workflows. This include facilities for moving objects onto desks, checkin, checkout, publishing and deploying. =head1 INTERFACE =head2 Public Class Methods =over 4 =item publish This method handles the publishing of story and media objects. Returns "publish_ids", an array of "story_id" and/or "media_id" integers published. The method accepts the following parameters: =over 4 =item story_id A single story to publish. =item media_id A single media object to publish. =item publish_ids A list of "story_id" and/or "media_id" elements to be published. =item publish_related_stories If this is set to true then related stories will be published too. In the web interface this happens if and only if the related stories have never been published before. This option is off by default. =item publish_related_media If this is set to true then related media will be published too. In the web interface this happens if and only if the related media objects have never been published before. This option is false by default. =item to_preview Set this to true to publish to the preview destination instead of the publish destination. This will fail if PREVIEW_LOCAL is On in bricolage.conf. =item publish_date The date and time (in ISO-8601 format) at which to publish the assets. =back Throws: =over =item Exception::AP =back B Stories and media have their publish_status field set to true. B The code for this method came mostly from F. It would be nice to collect this code in a module so it could be kept in one place. Notes about the notes. It's now F, This code is out of date since Mark's job-queue patch, as it still instantiates a $burner to publish immediately, so we need to update it. It's an opportunity to factor out the code into one place. =cut sub publish { my $pkg = shift; my $env = pop; my $args = $env->method || {}; my $method = 'publish'; print STDERR __PACKAGE__ . "->publish() called : args : ", Data::Dumper->Dump([$args],['args']) if DEBUG; # check for bad parameters for (keys %$args) { throw_ap(error => __PACKAGE__ . "::publish : unknown parameter \"$_\".") unless $pkg->is_allowed_param($_, $method); } my $pub_date = exists $args->{publish_date} ? local_date(xs_date_to_db_date($args->{publish_date}), ISO_8601_FORMAT) : strfdate(); my $preview = (exists $args->{to_preview} and $args->{to_preview}) ? 1 : 0; throw_ap(error => __PACKAGE__ . "::publish : cannot publish to_preview with " . "PREVIEW_LOCAL set.") if PREVIEW_LOCAL and $preview; my $published_only = exists($args->{published_only}) ? $args->{published_only} : 0; my @ids = _collect_ids("publish_ids", [ 'story_id', 'media_id' ], $env); # Instantiate the Burner object. my $burner = Bric::Util::Burner->new({ $preview ? (out_dir => PREVIEW_ROOT, user_id => get_user_id ) : (out_dir => STAGE_ROOT) }); # iterate through ids publishing shiznats my (%seen, @published, %desks); while (my $id = shift @ids) { my $obj; my $type; if ($id->name eq 'story_id') { $type = 'story'; $obj = Bric::Biz::Asset::Business::Story->lookup({ id => $id->value, ( $published_only ? ( published_version => 1) : ()) }); throw_ap(error => 'Unable to find story for story_id "' . $id->value . '".') unless $obj; } elsif ($id->name eq 'media_id') { $type = 'media'; $obj = Bric::Biz::Asset::Business::Media->lookup({ id => $id->value, ( $published_only ? ( published_version => 1 ) : ()) }); throw_ap(error => 'Unable to find media object for media_id "' . $id->value . '".') unless $obj; } else { throw_ap(error => "Unknown element found in publish_ids list."); } # don't need the object anymore $id = $id->value; # make sure we're not publishing stuff repeatedly next if $seen{$type}{$id}; $seen{$type}{$id} = 1; # check check check throw_ap(error => "Cannot publish checked-out $types{$type}: \"".$id."\".") if $obj->get_checked_out and not $preview; if (! $preview && !$published_only && $obj->get_workflow_id) { # It must be on a publish desk. my $did = $obj->get_desk_id; my $desk = $desks{$did} ||= Bric::Biz::Workflow::Parts::Desk->lookup({ id => $did }); # XXX There should always be a desk when there's a workflow, but # sometimes there isn't. Hence the "$desk &&" just lets it publish # if that's the case. It will be removed from workflow by the # publish. throw_ap qq{Cannot publish $types{$type} "$id" because it } . "is not on a publish desk" unless $desk && $desk->can_publish; } # Check for PUBLISH permission, or READ if previewing throw_ap(error => "Access to publish $types{$type} \"$id\" denied.") unless chk_authz($obj, PUBLISH, 1) or ($preview and chk_authz($obj, READ, 1)); # schedule related stuff if requested if ($args->{publish_related_stories} or $args->{publish_related_media}) { # loop through related objects, adding to the todo list as # appropriate foreach my $rel ($obj->get_related_objects) { # Skip documents whose current version has already been # published. next unless $rel->needs_publish; # Skip deactivated documents. next unless $rel->is_active; # Add it in. if ($args->{publish_related_stories} && UNIVERSAL::isa($rel, 'Bric::Biz::Asset::Business::Story')) { push(@ids, name(story_id => $rel->get_id)); } elsif ($args->{publish_related_media} && UNIVERSAL::isa($rel, 'Bric::Biz::Asset::Business::Media')) { push(@ids, name(media_id => $rel->get_id)); } else { # Nothing. } } } if ($preview) { # Just preview it. push @published, name( "$type\_id" => $id ) if $burner->preview($obj, $type, get_user_id); } else { # Schedule the publish. my $name = 'Publish "' . $obj->get_name . '"'; my $job = Bric::Util::Job::Pub->new({ sched_time => $args->{publish_date}, user_id => get_user_id, name => $name, "$type\_id" => $obj->get_id, priority => $obj->get_priority, })->save; log_event('job_new', $job); push @published, name( "$type\_id" => $id ); } } print STDERR __PACKAGE__ . "->publish() finished : ", join(', ', map { $_->name . " => " . $_->value } @published), "\n" if DEBUG; # name, type and return return name(publish_ids => \@published); } =item deploy This method handles deploying templates. The method returns "deploy_ids", a list of "template_id" integers deployed on success. The method accepts the following parameters: =over 4 =item template_id A single template to publish. =item deploy_ids A list of "template_id" elements to be published. =back Throws: =over =item Exception::AP =back Side Effects: Templates have their deploy_status set to true. Notes: Code here comes from comp/widgets/desk/callback.mc. It might be cool to move this code into a module so it could be shared. It's not nearly as gnarly as the publish() code though. =cut sub deploy { my $pkg = shift; my $env = pop; my $args = $env->method || {}; my $method = 'deploy'; print STDERR __PACKAGE__ . "->deploy() called : args : ", Data::Dumper->Dump([$args],['args']) if DEBUG; # check for bad parameters for (keys %$args) { throw_ap(error => __PACKAGE__ . "::deploy : unknown parameter \"$_\".") unless $pkg->is_allowed_param($_, $method); } my @ids = _collect_ids("deploy_ids", [ "template_id" ], $env); my $burner = Bric::Util::Burner->new; foreach my $id (map { $_->value } @ids) { my $fa = Bric::Biz::Asset::Template->lookup({ id => $id }); throw_ap(error => "Unable to find template for template_id \"$id\".") unless $fa; # check check check throw_ap(error => "Cannot deloy checked-out template : \"$id\".") if $fa->get_checked_out; # Check for PUBLISH permission throw_ap(error => "Access denied.") unless chk_authz($fa, PUBLISH, 1); $burner->deploy($fa); log_event($fa->get_deploy_status ? 'template_redeploy' : 'template_deploy', $fa); $fa->set_deploy_date(strfdate()); $fa->set_deploy_status(1); # Remove it from the current desk. if (my $desk = $fa->get_current_desk) { $desk->remove_asset($fa); $desk->save; } # Clear the workflow ID. if ($fa->get_workflow_id) { $fa->set_workflow_id(undef); log_event("template_rem_workflow", $fa); } $fa->save; } print STDERR __PACKAGE__ . "->deploy() finished : ", join(', ', map { $_->name . " => " . $_->value } @ids), "\n" if DEBUG; return name(deploy_ids => \@ids); } =item checkout This method checks out a story, media and/or template objects. After this call the objects are visible on the user's workspace in the web interface and are not available for other users to edit. An error will result if you try to checkout an object that is not checked in. The method returns a list of ids checked out on success. The method accepts the following parameters: =over 4 =item story_id A single story to checkout. =item media_id A single media object to checkout. =item template_id A single template object to checkout. =item checkout_ids A list of "story_id", "template_id" and/or "media_id" elements to be checked out. =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: NONE =cut sub checkout { my $pkg = shift; my $env = pop; my $args = $env->method || {}; my $method = 'checkout'; print STDERR __PACKAGE__ . "->checkout() called : args : ", Data::Dumper->Dump([$args],['args']) if DEBUG; # check for bad parameters for (keys %$args) { throw_ap(error => __PACKAGE__ . "::checkout : unknown parameter \"$_\".") unless $pkg->is_allowed_param($_, $method); } my @ids = _collect_ids("checkout_ids", [ "story_id", "media_id", "template_id" ], $env); my %seen; foreach my $id (@ids) { my $obj; my $type; if ($id->name eq 'story_id') { $type = 'story'; $obj = Bric::Biz::Asset::Business::Story->lookup( { id => $id->value }); throw_ap(error => "Unable to find story for story_id \"".$id->value."\".") unless $obj; } elsif ($id->name eq 'media_id') { $type = 'media'; $obj = Bric::Biz::Asset::Business::Media->lookup( { id => $id->value }); throw_ap(error => "Unable to find media object for media_id \"".$id->value."\".") unless $obj; } elsif ($id->name eq 'template_id') { $type = 'template'; $obj = Bric::Biz::Asset::Template->lookup( { id => $id->value }); throw_ap(error => "Unable to find template object for template_id \"".$id->value."\".") unless $obj; } else { throw_ap(error => "Unknown element found in checkout_ids list."); } # check check check throw_ap(error => "Cannot check-out already checked-out $types{$type}: \"".$id->value."\".") if $obj->get_checked_out; # make sure we're not trying to checkout stuff repeatedly next if $seen{$type}{$id}; $seen{$type}{$id} = 1; # might need to assign a workflow here, if this item was just # published, for example. if ($obj->get_workflow_id) { # Check for EDIT permission throw_ap(error => "Access denied.") unless chk_authz($obj, EDIT, 1); } else { # Check for RECALL permission throw_ap(error => "Access denied.") unless chk_authz($obj, RECALL, 1); my $workflow = (Bric::Biz::Workflow->list ({ type => $wf_types{$type} }))[0]; $obj->set_workflow_id($workflow->get_id); log_event("${type}_add_workflow", $obj, { Workflow => $workflow->get_name }); my $desk = $workflow->get_start_desk; $desk->accept({'asset' => $obj}); $desk->save; log_event("${type}_moved", $obj, { Desk => $desk->get_name }); } # check 'em out $obj->checkout({user__id => get_user_id}); $obj->save; # log the checkout log_event("${type}_checkout", $obj); } print STDERR __PACKAGE__ . "->checkout() finished : ", join(', ', map { $_->name . " => " . $_->value } @ids), "\n" if DEBUG; return name(checkout_ids => \@ids); } =item checkin This method checks in a story, media and/or template objects. After this call the objects are no longer visible on the user's workspace in the web interface and are available for other users to edit. An error will result if you try to checkin an object that is not checked out. The method returns a list of ids checked in. The method accepts the following parameters: =over 4 =item story_id A single story to checkin. =item media_id A single media object to checkin. =item template_id A single template object to checkin. =item checkin_ids A list of "story_id", "template_id" and/or "media_id" elements to be checked in. =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: NONE =cut sub checkin { my $pkg = shift; my $env = pop; my $args = $env->method || {}; my $method = 'checkin'; print STDERR __PACKAGE__ . "->checkin() called : args : ", Data::Dumper->Dump([$args],['args']) if DEBUG; # check for bad parameters for (keys %$args) { throw_ap(error => __PACKAGE__ . "::checkin : unknown parameter \"$_\".") unless $pkg->is_allowed_param($_, $method); } my @ids = _collect_ids("checkin_ids", [ "story_id", "media_id", "template_id" ], $env); my %seen; foreach my $id (@ids) { my $obj; my $type; if ($id->name eq 'story_id') { $type = 'story'; $obj = Bric::Biz::Asset::Business::Story->lookup( { id => $id->value }); throw_ap(error => "Unable to find story for story_id \"".$id->value."\".") unless $obj; } elsif ($id->name eq 'media_id') { $type = 'media'; $obj = Bric::Biz::Asset::Business::Media->lookup( { id => $id->value }); throw_ap(error => "Unable to find media object for media_id \"".$id->value."\".") unless $obj; } elsif ($id->name eq 'template_id') { $type = 'template'; $obj = Bric::Biz::Asset::Template->lookup( { id => $id->value }); throw_ap(error => "Unable to find template object for template_id \"".$id->value."\".") unless $obj; } else { throw_ap(error => "Unknown element found in checkin_ids list."); } # check check check throw_ap(error => "Cannot check-in non checked-out $types{$type}: \"".$id->value."\".") unless $obj->get_checked_out; # Check for EDIT permission throw_ap(error => "Access denied.") unless chk_authz($obj, EDIT, 1); # make sure we're not trying to checkin stuff repeatedly next if $seen{$type}{$id}; $seen{$type}{$id} = 1; # check that we have a desk my $curr_desk = $obj->get_current_desk; throw_ap(error => "Cannot check-in $types{$type} without a current desk: \"".$id->value."\".") unless $curr_desk; # check 'em in $obj->checkin; $obj->save; # log the checkin log_event("${type}_checkin", $obj, { Version => $obj->get_version }); } print STDERR __PACKAGE__ . "->checkin() finished : ", join(', ', map { $_->name . " => " . $_->value } @ids), "\n" if DEBUG; return name(checkin_ids => \@ids); } =item move This method moves objects between workflows and desks. The method returns a list of ids moved. The method accepts the following parameters: =over 4 =item desk (required) The name of the desk to move to. =item workflow The name of the workflow to move to. If this is unspecified then desk must refer to a desk in the current workflow for the object. If specified then only one type of object can be successfully moved since workflows are type-specific, I think. =item story_id A single story to move. =item media_id A single media object to move. =item template_id A single template object to move. =item move_ids A list of "story_id", "template_id" and/or "media_id" elements to be checked in. =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: NONE =cut sub move { my $pkg = shift; my $env = pop; my $args = $env->method || {}; my $method = 'move'; print STDERR __PACKAGE__ . "->move() called : args : ", Data::Dumper->Dump([$args],['args']) if DEBUG; # check for bad parameters for (keys %$args) { throw_ap(error => __PACKAGE__ . "::move : unknown parameter \"$_\".") unless $pkg->is_allowed_param($_, $method); } # make sure we have a desk throw_ap(error => __PACKAGE__ . "::move : missing required parameter \"desk\".") unless $args->{desk}; # find destination workflow if defined my $to_workflow; if (exists $args->{workflow}) { ($to_workflow) = Bric::Biz::Workflow->list({ name => $args->{workflow}, }); throw_ap(error => __PACKAGE__ . "::move : no workflow found matching " . "(workflow => \"$args->{workflow}\")") unless defined $to_workflow; } # find destination desk my ($to_desk) = Bric::Biz::Workflow::Parts::Desk->list({ name => $args->{desk}, }); throw_ap(error => __PACKAGE__ . "::move : no desk found matching " . "(desk => \"$args->{desk}\")") unless $to_desk; my @ids = _collect_ids( "move_ids", [ "story_id", "media_id", "template_id" ], $env ); foreach my $id (@ids) { my $obj; my $type; if ($id->name eq 'story_id') { $type = 'story'; $obj = Bric::Biz::Asset::Business::Story->lookup({ id => $id->value, }); throw_ap(error => "Unable to find story for story_id \"".$id->value."\".") unless $obj; } elsif ($id->name eq 'media_id') { $type = 'media'; $obj = Bric::Biz::Asset::Business::Media->lookup({ id => $id->value, }); throw_ap(error => "Unable to find media object for media_id \"".$id->value."\".") unless $obj; } elsif ($id->name eq 'template_id') { $type = 'template'; $obj = Bric::Biz::Asset::Template->lookup({ id => $id->value }); throw_ap 'Unable to find template object for template_id "' . $id->value .'".' unless $obj; } else { throw_ap(error => "Unknown element found in move_ids list."); } # check check check throw_ap(error => "Cannot move checked-out $types{$type}: \"" . $id->value."\".") if $obj->get_checked_out; # Check for EDIT permission throw_ap(error => "Access denied.") unless chk_authz($obj, EDIT, 1); # are we moving to a new workflow? if ($to_workflow) { # check the type my $ok = 0; if ($type eq 'story') { $ok = 1 if $to_workflow->get_type == STORY_WORKFLOW; } elsif ($type eq 'media') { $ok = 1 if $to_workflow->get_type == MEDIA_WORKFLOW; } else { $ok = 1 if $to_workflow->get_type == TEMPLATE_WORKFLOW; } throw_ap(error => __PACKAGE__ . "::move : cannot move $types{$type} \"" . $id->value . "\" to " . "workflow \"$args->{workflow}\" : type mismatch.") unless $ok; # move to new workflow $obj->set_workflow_id($to_workflow->get_id); log_event("${type}_add_workflow", $obj, { Workflow => $to_workflow->get_name }); } else { # might need to assign a workflow here, if this item was just # published, for example. unless ($obj->get_workflow_id) { my $workflow = (Bric::Biz::Workflow->list({ type => $wf_types{$type}, }))[0]; $obj->set_workflow_id($workflow->get_id); log_event("${type}_add_workflow", $obj, { Workflow => $workflow->get_name, }); my $desk = $workflow->get_start_desk; $desk->accept({asset => $obj}); $desk->save; log_event("$type\_moved", $obj, { Desk => $desk->get_name }); } } # get origin desk my $from_desk = $obj->get_current_desk; throw_ap(error => "Cannot move $types{$type} without a current desk: \"" . $id->value . "\".)") unless $from_desk; # don't move if we're already here unless ($from_desk->get_id == $to_desk->get_id) { $from_desk->transfer({ asset => $obj, to => $to_desk, }); $from_desk->save; $to_desk->save; } $obj->save; # log the move log_event("$type\_moved", $obj, { Desk => $to_desk->get_name }); } print STDERR __PACKAGE__ . "->move() finished : ", join(', ', map { $_->name . " => " . $_->value } @ids), "\n" if DEBUG; return name(move_ids => \@ids); } =item list_ids This method queries the database for matching workflows and returns a list of ids. If no workflows are found an empty list will be returned. This method can accept the following named parameters to specify the search. Some fields support matching and are marked with an (M). The value for these fields will be interpreted as an SQL match expression and will be matched case-insensitively. Other fields must specify an exact string to match. Match fields combine to narrow the search results (via ANDs in an SQL WHERE clause). =over 4 =item name (M) The workflow's name. =item description The workflow's description. =item site The workflow's site name. =item type Return workflows of type 'Story', 'Media', or 'Template'. By default all workflow types are returned. =item desk Given a desk name, return workflows that contain this desk. =item active Set false to return deleted workflows. Returns only active workflows by default. =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: NONE =cut sub list_ids { my $pkg = shift; my $env = pop; my $args = $env->method || {}; my $method = 'list_ids'; my $module = $pkg->module; print STDERR __PACKAGE__ . "->$method() called : args : ", Data::Dumper->Dump([$args],['args']) if DEBUG; # check for bad parameters for (keys %$args) { throw_ap(error => __PACKAGE__ . "::$method : unknown parameter \"$_\".") unless $pkg->is_allowed_param($_, $method); } # convert site name to site_id if (exists $args->{site}) { my $site = delete $args->{site}; my $site_id = Bric::Biz::Site->list_ids({ name => $site }); if (defined $site_id) { $args->{site_id} = $site_id->[0]; } else { throw_ap error => __PACKAGE__ . "::$method: unknown site \"" . $args->{site} . "\"."; } } # convert type to integer if (exists $args->{type}) { if (exists $wf_types{$args->{type}}) { my $type = $wf_types{lc($args->{type})}; $args->{type} = $type; } else { throw_ap error => __PACKAGE__ . "::$method: invalid type \"" . $args->{type} . "\"."; } } # convert desk name to desk_id if (exists $args->{desk}) { my $desk = delete $args->{desk}; my $desk_id = Bric::Biz::Workflow::Parts::Desk->list_ids({ name => $desk }); if (defined $desk_id) { $args->{desk_id} = $desk_id->[0]; } else { throw_ap error => __PACKAGE__ . "::$method: unknown desk \"" . $args->{desk} . "\"."; } } $args->{active} = 1 unless exists $args->{active}; my @ids = $pkg->class->list_ids($args); # name the results my @result = map { name("$module\_id" => $_) } @ids; # name the array and return return name("$module\_ids" => \@result); } =item export The export method retrieves a set of assets from the database, serializes them and returns them as a single XML document. See L for the schema of the returned document. Accepted paramters are: =over 4 =item workflow_id Specifies a single workflow_id to be retrieved. =item workflow_ids Specifies a list of workflow_ids. The value for this option should be an array of integer "workflow_id" assets. =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: NONE =cut =item create The create method creates new objects using the data contained in an XML document of the format created by export(). Returns a list of new ids created in the order of the assets in the document. Available options: =over 4 =item document (required) The XML document containing objects to be created. The document must contain at least one asset object. =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: NONE =cut =item update The update method updates an asset using the data in an XML document of the format created by export(). A common use of update() is to export() a selected object, make changes to one or more fields and then submit the changes with update(). Returns a list of new ids created in the order of the assets in the document. Takes the following options: =over 4 =item document (required) The XML document where the objects to be updated can be found. The document must contain at least one asset and may contain any number of related asset objects. =item update_ids (required) A list of "workflow_id" integers for the assets to be updated. These must match id attributes on asset elements in the document. If you include objects in the document that are not listed in update_ids then they will be treated as in create(). For that reason an update() with an empty update_ids list is equivalent to a create(). =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: NONE =cut =item delete The delete() method deletes assets. It takes the following options: =over 4 =item workflow_id Specifies a single asset ID to be deleted. =item workflow_ids Specifies a list of asset IDs to delete. =back Throws: =over =item Exception::AP =back Side Effects: NONE Notes: The ONLY reason this method needs overridden is because of the __WORKFLOWS__ cache. =cut sub delete { my $pkg = shift; my $env = pop; my $args = $env->method || {}; my $module = $pkg->module; my $method = 'delete'; my $cache = Bric::App::Cache->new; print STDERR "$pkg\->$method() called : args : ", Data::Dumper->Dump([$args],['args']) if DEBUG; # check for bad parameters for (keys %$args) { throw_ap(error => "$pkg\::$method : unknown parameter \"$_\".") unless $pkg->is_allowed_param($_, $method); } # sugar for one id $args->{"$module\_ids"} = [ $args->{"$module\_id"} ] if exists $args->{"$module\_id"}; # make sure asset_ids is an array throw_ap(error => "$pkg\::$method : missing required $module\_id(s) setting.") unless defined $args->{"$module\_ids"}; throw_ap(error => "$pkg\::$method : malformed $module\_id(s) setting.") unless ref $args->{"$module\_ids"} and ref $args->{"$module\_ids"} eq 'ARRAY'; # delete the asset foreach my $id (@{$args->{"$module\_ids"}}) { print STDERR "$pkg\->$method() : deleting $module\_id $id\n" if DEBUG; # lookup the asset my $asset = $pkg->class->lookup({ id => $id }); throw_ap(error => "$pkg\::$method : no $module found for id \"$id\"") unless $asset; throw_ap(error => "$pkg\::$method : access denied for $module \"$id\".") unless chk_authz($asset, EDIT, 1); # delete the asset $asset->deactivate; $asset->save; log_event("$module\_deact", $asset); $cache->set('__WORKFLOWS__' . $asset->get_site_id, 0); } return name(result => 1); } =item $self->module Returns the module name, that is the first argument passed to bric_soap. =cut sub module { 'workflow' } =item is_allowed_param =item $pkg->is_allowed_param($param, $method) Returns true if $param is an allowed parameter to the $method method. =cut sub is_allowed_param { my ($pkg, $param, $method) = @_; my $module = $pkg->module; my $allowed = { publish => { map { $_ => 1 } qw(story_id media_id publish_ids publish_related_stories publish_related_media publish_date published_only to_preview) }, deploy => { map { $_ => 1 } qw(template_id deploy_ids) }, checkout => { map { $_ => 1 } qw(story_id media_id template_id checkout_ids) }, checkin => { map { $_ => 1 } qw(story_id media_id template_id checkin_ids) }, move => { map { $_ => 1 } qw(story_id media_id template_id move_ids desk workflow) }, list_ids => { map { $_ => 1 } qw(name description site type desk active) }, export => { map { $_ => 1 } ("$module\_id", "$module\_ids") }, create => { map { $_ => 1 } qw(document) }, update => { map { $_ => 1 } qw(document update_ids) }, delete => { map { $_ => 1 } ("$module\_id", "$module\_ids") }, }; return exists($allowed->{$method}->{$param}); } =back =head2 Private Class Methods =over 4 =item $pkg->load_asset($args) This method provides the meat of both create() and update(). The only difference between the two methods is that update_ids will be empty on create(). =cut sub load_asset { my ($pkg, $args) = @_; my $document = $args->{document}; my $data = $args->{data}; my %to_update = map { $_ => 1 } @{$args->{update_ids}}; my $module = $pkg->module; # parse and catch errors unless ($data) { eval { $data = parse_asset_document($document, $module, 'desk') }; throw_ap(error => __PACKAGE__ . " : problem parsing asset document : $@") if $@; throw_ap(error => __PACKAGE__ . " : problem parsing asset document : no $module found!") unless ref $data and ref $data eq 'HASH' and exists $data->{$module}; print STDERR Data::Dumper->Dump([$data],['data']) if DEBUG; } # loop over workflows, filling @ids my (@ids, %paths); foreach my $adata (@{ $data->{$module} }) { my $id = $adata->{id}; # are we updating? my $update = exists $to_update{$id}; # get object my $asset; unless ($update) { # create empty workflow $asset = $pkg->class->new; throw_ap(error => __PACKAGE__ . " : failed to create empty $module object.") unless $asset; print STDERR __PACKAGE__ . " : created empty module object\n" if DEBUG; } else { # updating $asset = $pkg->class->lookup({ id => $id }); throw_ap(error => __PACKAGE__ . "::update : no $module found for \"$id\"") unless $asset; } throw_ap(error => __PACKAGE__ . " : access denied.") unless chk_authz($asset, CREATE, 1); $adata->{site} = 'Default Site' unless exists $adata->{site}; (my $look = $adata->{site}) =~ s/([_%\\])/\\$1/g; my $site = Bric::Biz::Site->lookup({ name => $look }); unless (defined $site) { throw_ap error => __PACKAGE__ . ": site \"" . $adata->{site} . "\" not found."; } # don't create a workflow that's already taken my @wfs = ($pkg->class->list_ids({ name => $adata->{name}, site_id => $site->get_id }), $pkg->class->list_ids({ name => $adata->{name}, site_id => $site->get_id, active => 0 }) ); if (@wfs > 1) { throw_ap error => __PACKAGE__ . ": can't create/update existing inactive workflow " . '"' . $adata->{name} . '" in site "' . $adata->{site} . '".'; } elsif (@wfs == 1 && !$update) { throw_ap error => __PACKAGE__ . "::create: existing active workflow " . '"' . $adata->{name} . '" in site "' . $adata->{site} . '".'; } elsif (@wfs == 1 && $update && $wfs[0] != $id) { throw_ap error => __PACKAGE__ . "::update: existing active workflow " . '"' . $adata->{name} . '" in site "' . $adata->{site} . '".'; } my $type = lc $adata->{type}; if (exists $wf_types{$type}) { $type = $wf_types{$type}; } else { throw_ap error => __PACKAGE__ . ": invalid type \"$type\"."; } # set simple fields $asset->set_name($adata->{name}); $asset->set_description($adata->{description}); $asset->set_site_id($site->get_id); $asset->set_type($type); # desks if ($update) { # update desks my %old_desks = map { $_->get_name => $_ } $asset->allowed_desks; my %new_desks = map { (ref($_) ? $_->{content} : $_) => $_ } @{ $adata->{desks}{desk} }; # delete any desks not in the XML foreach my $old_name (keys %old_desks) { unless (exists $new_desks{$old_name}) { # check if the desk has assets my $old_desk = $old_desks{$old_name}; if ($old_desk->assets) { throw_ap error => __PACKAGE__ . '::update: desk ' . "\"$old_name\" can't be deleted - it has assets." } else { $asset->del_desk([$old_desk]); log_event('workflow_del_desk', $asset, { Desk => $old_name }); } } } # add any new desks foreach my $new_name (keys %new_desks) { unless (exists $old_desks{$new_name}) { my $new_ddata = $new_desks{$new_name}; _add_desk($asset, $new_ddata, $new_name); } } } else { # create - add desks foreach my $ddata (@{ $adata->{desks}{desk} }) { my $name = ref($ddata) ? $ddata->{content} : $ddata; _add_desk($asset, $ddata, $name); } } # save $asset->save(); log_event("$module\_" . ($update ? 'save' : 'new'), $asset); # all done push(@ids, $asset->get_id); } return name(ids => [ map { name("$module\_id" => $_) } @ids ]); } =item $pkg->serialize_asset( writer => $writer, workflow_id => $id, args => $args) Serializes a single workflow object into a workflow using the given writer and args. =cut sub serialize_asset { my $pkg = shift; my %options = @_; my $module = $pkg->module; my $id = $options{"$module\_id"}; my $writer = $options{writer}; my $asset = $pkg->class->lookup({id => $id}); throw_ap(error => __PACKAGE__ . "::export : $module\_id \"$id\" not found.") unless $asset; throw_ap(error => __PACKAGE__ . "::export : access denied for $module \"$id\".") unless chk_authz($asset, READ, 1); # open workflow element $writer->startTag($module, id => $id); my $site = Bric::Biz::Site->lookup({ id => $asset->get_site_id }); # write out simple attributes in schema order $writer->dataElement(name => $asset->get_name); $writer->dataElement(description => $asset->get_description); $writer->dataElement(site => $site->get_name); $writer->dataElement(type => WORKFLOW_TYPE_MAP->{$asset->get_type}); $writer->dataElement(active => ($asset->is_active ? 1 : 0)); # write out desks my @desks = grep { chk_authz($_, READ, 1) } $asset->allowed_desks; $writer->startTag('desks'); foreach my $desk (@desks) { my $is_start = $asset->is_start_desk($desk); my $is_pub = $desk->can_publish; $writer->dataElement(desk => $desk->get_name, ($is_start ? (start => 1) : ()), ($is_pub ? (publish => 1) : ()) ); } $writer->endTag('desks'); # close workflow element $writer->endTag($module); } =item @ids = _collect_ids("publish_ids", [ "story_id", "media_id" ], $env); This method takes care of extracting a collating the id parameters accepted by the above methods. The result is an array of SOAP::Data objects with name() and value() set accordingly. Throws: NONE Side Effects: NONE Notes: I bet this method is inefficient. Using XPath syntax just I slow... =cut sub _collect_ids { my ($list, $single, $env) = @_; my @ids; # operate on the method my $meth = $env->match('/Envelope/Body/[1]'); # find single params and collect their SOAP::Data representations foreach (@$single) { my $data = $meth->dataof($_); push(@ids, $data) if $data; } # switch to list arg, if available my $list_meth = $env->match('/Envelope/Body/[1]/' . $list); if ($list_meth) { # iterate through subelements collecting SOAP::Data objects my ($data, $count); for ($count = 1; $data = $list_meth->dataof("[${count}]"); $count++) { # should I check that $data->name() is within @$single here? push(@ids, $data); } } return @ids; } =begin comment Private function to add a desk to a workflow during create/update =end comment =cut sub _add_desk { my ($asset, $ddata, $name) = @_; (my $look = $name) =~ s/([_%\\])/\\$1/g; my $desk = Bric::Biz::Workflow::Parts::Desk->lookup({ name => $look }); unless (defined $desk) { # desk doesn't exist, so create it my $is_publish = (ref($$ddata) && exists($ddata->{publish}) && $ddata->{publish}) ? 1 : 0; $desk = Bric::Biz::Workflow::Parts::Desk->new({ name => $name, publish => $is_publish, }); $desk->save; log_event('desk_new', $desk); # note: if the desk already exists, I don't think we should # change its publishability here based on the publish attribute # because the desk might be on other workflows } # add the desk to the workflow $asset->add_desk({ allowed => [$desk] }); log_event('workflow_add_desk', $asset, { Desk => $name }); # set start desk if there's a start attribute; # I guess if they put multiple start desks, # we'll just go with the last one if (ref($ddata) && exists($ddata->{start})) { $asset->set_start_desk($desk); } } =back =head1 AUTHOR Sam Tregar Scott Lanning =head1 SEE ALSO L, L =cut 1;