::xo::library doc {
XoWiki Workflow - main library classes and objects
@author Gustaf Neumann
@creation-date 2008-03-05
}
# TODO:
# - after import, references are not updated
# (same for plain references); after_import methods?
#
# - Roles
# - assignment
# - workflow-assignment includelet (over multiple workflows and
# package instances)
::xo::db::require package xowiki
::xo::library require -package xowiki xowiki-procs
::xo::library require -package xotcl-core 06-package-procs
::xo::library require -package xowiki menu-procs
namespace eval ::xowf {
#
# Should we use a shared or a per-context workflow definition.
#
set ::xowf::sharedWorkflowDefinition 1
::xo::PackageMgr create ::xowf::Package \
-package_key "xowf" -pretty_name "XoWiki Workflow" \
-superclass ::xowiki::Package
Package site_wide_package_parameter_page_info {
name en:xowf-site-wide-parameter
title "Xowf Site-wide Parameter"
instance_attributes {
index_page table-of-contents
MenuBar t
top_includelet none
production_mode t
with_user_tracking t with_general_comments f with_digg f with_tags f
with_delicious f with_notifications f
security_policy ::xowiki::policy1
}}
Package site_wide_package_parameters {
parameter_page en:xowf-site-wide-parameter
}
Package site_wide_pages {
Workflow.form
atjob-form
TestItemText.form
TestItemShortText.form
TestItemMC.form
TestItemSC.form
TestItemReorder.form
TestItemUpload.form
TestItemComposite.form
TestItemPoolQuestion.form
ExamFolder
online-exam.wf
inclass-quiz.wf
inclass-exam.wf
edit-interaction.wf
edit-grading-scheme.wf
answer-single-question.wf
topic-assignment.wf
quiz-select_question.form
select_question.form
select-topics.form
select-group-members.form
}
Package default_package_parameters {
parameter_page en:xowf-default-parameter
}
Package default_package_parameter_page_info {
name en:xowf-default-parameter
title "Xowf Default Parameter"
instance_attributes {
MenuBar t top_includelet none production_mode t with_user_tracking t with_general_comments f
with_digg f with_tags f
ExtraMenuEntries {{config -use xowf}}
with_delicious f with_notifications f security_policy ::xowiki::policy1
}
}
Package ad_proc create_new_workflow_page {
-package_id:required
-parent_id:required
-name:required
-title:required
{-instance_attributes ""}
} {
Helper proc for loading workflow prototype page with less effort.
} {
#
# Load Workflow.form
#
xo::Package require $package_id
set item_ref_info [$package_id item_ref -use_site_wide_pages true -default_lang en \
-parent_id $parent_id \
en:Workflow.form]
set page_template [dict get $item_ref_info item_id]
if {$page_template != 0} {
#
# Create FormPage
#
set p [::xowiki::FormPage new \
-name $name \
-title $title \
-set text {} \
-instance_attributes $instance_attributes \
-page_template $page_template]
} else {
ns_log error "could not load Workflow form, therefore, creation of workflow $name failed as well"
set p ""
}
return $p
}
Package ad_instproc initialize {} {
Add mixin ::xowf::WorkflowPage to every FormPage.
} {
#
# This method is called, whenever an xowf package is initialized.
#
next
#:msg "::xowiki::FormPage instmixin add ::xowf::WorkflowPage"
::xowiki::FormPage instmixin add ::xowf::WorkflowPage
}
Package instproc call {object method options} {
if {[$object istype ::xowiki::FormPage]} {
if {[$object is_wf_instance]} {
set ctx [::xowf::Context require $object]
#:msg "wfi: creating context form object $object = $ctx, chlds=[$ctx info children]"
#:msg "wfi: o $object has mixins [$object info mixin]"
} elseif {[$object is_wf]} {
set ctx [::xowf::Context require $object]
#:msg "wf: creating context form object $object = $ctx, chlds=[$ctx info children]"
#:msg "wf: o $object has mixins [$object info mixin]"
}
}
next
}
Package ad_instproc destroy {} {
remove mixin
} {
#
# In general, it is possible, that multiple xowf packages are
# concurrently active in one thread. We want to remove the mixin
# only, when the last instance is deleted.
#
if {[llength [[self class] allinstances]] == 1} {
::xowiki::FormPage instmixin delete ::xowf::WorkflowPage
}
next
}
# Package instproc delete {-item_id -name} {
# # Provide a method to delete the foreign key references, when
# # an item for an atjob is deleted. We do here the same magic
# # as in ::xowiki::Package to obtain the item_id
# if {![info exists item_id]} {set item_id [:query_parameter item_id:int32]}
# if {$item_id ne ""} {
# db_dml dbqd..xowf_delete "delete from xowf_atjob where owner_id = :item_id"
# }
# next
# }
#
# Most primitive class, used it for WorkflowConstructs (things, user
# can write in their workflow definitions) and WorkflowContainer.
#
::xotcl::Class create WorkflowObject
WorkflowObject instproc wf_context {} {
#
# Try to determine the workflow context via call-stack.
#
set max [info level]
for {set i 0} {$i < $max} {incr i} {
if {![catch {set s [uplevel $i self]} msg]} {
set obj [lindex $s 0]
if {[$obj istype ::xowf::Context]} {
#:log "$obj [nsf::is object $obj] precedence: [$obj info precedence]"
return $obj
}
if {[$obj hasclass ::xowf::WorkflowPage]} {
return [$obj wf_context]
}
}
}
#
# If everything fails, fall back to the old-style method, which is
# incorrect for shared workflow definitions. This fallback is
# just for transitional code.
#
ad_log warning "cannot determine wf_context from call-stack"
return [:info parent]
}
if {$::xowf::sharedWorkflowDefinition > 0} {
#
# Workflow Container
#
Class create WorkflowContainer -superclass WorkflowObject -parameter {
{autoname}
{auto_form_constraints ""}
{auto_form_template ""}
{debug 0}
{shared_definition 1}
}
WorkflowContainer instproc object-specific args {
#
# make sure, we have always a value
#
if {![info exists :object-specific]} {
set :object-specific ""
}
#
# If called without args, return the current value, otherwise
# aggregated the values.
#
set l [llength $args]
switch $l {
0 {
#
# Called without args, return the current value
#
return ${:object-specific}
}
1 {
#
# Called with a single value, aggregate partial values (and
# separate these by an empty line for safety)
#
append :object-specific \n [lindex $args 0]
#ns_log notice "=== object-specific [self] ${:object-specific}"
}
default {
error "wrong number of arguments"
}
}
}
WorkflowContainer instproc init {} {
set :creation_time [clock seconds]
::xo::add_cleanup [self] [list [self] cleanup]
next
}
WorkflowContainer instproc cleanup {} {
#
# Keep workflow container 10 minutes in the per-thread cache.
#
if {[clock seconds] - ${:creation_time} > 600} {
#ns_log notice "======================== WorkflowContainer [self] self destroys"
::xo::remove_cleanup [self]
:destroy
}
}
WorkflowContainer instproc object {} {
#
# Method for emulating "object". Object specific code cannot
# work in shared workflow definitions, since one workflow
# definition is used in the shared case for many objects at the
# same time. Object specific code should use the
# "object-specific" method below.
#
# Here we fall back to the unshared case
#
set ctx [:wf_context]
set object [$ctx object]
set template [$object page_template]
if {${:shared_definition}} {
ns_log warning "Workflow $template [$template name] does not work with\
shared definitions since it refers to 'object'; fall back to unshared definition"
set :shared_definition 0
}
return $object
}
}
#
# Workflow Context
#
Class create Context -parameter {
{current_state "[self]::initial"}
workflow_definition
object
{all_roles false}
{default_definition ""}
in_role
wf_container
}
# forward property management to the workflow object
Context instforward property {%set :object} %proc
Context instforward get_property {%set :object} %proc
Context instforward set_property {%set :object} %proc
Context instforward set_new_property {%set :object} set_property -new 1
# forward form_constraints, view_method, form, and form_loader the to current state object
Context instforward get_form_constraints {%set :current_state} form_constraints
Context instforward get_view_method {%set :current_state} view_method
Context instforward form {%set :current_state} form
Context instforward form_loader {%set :current_state} form_loader
#
# The following methods autoname, auto_form_constraints,
# auto_form_template, and debug contain legacy access methods for
# cases, where no shared workflow definition is used.
#
Context instproc autoname {} {
#
# We want to distinguish between a set "autoname" and an
# unspecified "autoname". Therefore, we do not want to use a
# default in the WorkflowContainer
#
if {${:wf_container} ne [self]} {
if {[${:wf_container} exists autoname]} {
return [${:wf_container} autoname]
}
} elseif {[info exists :autoname]} {
return ${:autoname}
}
return "f"
}
Context instproc auto_form_constraints {} {
if {${:wf_container} ne [self]} {
return [${:wf_container} auto_form_constraints]
} elseif {[info exists :auto_form_constraints]} {
return ${:auto_form_constraints}
}
return ""
}
Context instproc auto_form_template {} {
if {${:wf_container} ne [self]} {
return [${:wf_container} auto_form_template]
} elseif {[info exists :auto_form_template]} {
return ${:auto_form_template}
}
return ""
}
Context instproc debug {} {
if {${:wf_container} ne [self]} {
return [${:wf_container} debug]
} elseif {[info exists :debug]} {
return ${:debug}
}
return 0
}
Context instproc object-specific {code} {
#:log "=== legacy call <$code>"
:uplevel [list ${:object} eval $code]
}
#
# container specific methods on Context
#
Context instproc wf_definition_object {name} {
return ${:wf_container}::$name
}
Context instproc set_current_state {value} {
set :current_state ${:wf_container}::$value
}
Context instproc get_current_state {} {
namespace tail ${:current_state}
}
Context instproc get_actions {} {
set actions [list]
foreach action [${:current_state} get_actions] {
lappend actions ${:wf_container}::$action
}
#:msg "for ${:current_state} actions '$actions"
return $actions
}
Context instproc defined {what} {
set result [list]
foreach c [${:wf_container} info children] {
if {[$c istype $what]} {lappend result $c}
}
return $result
}
Context instproc resolve_form_name {-object:required name} {
set package_id [$object package_id]
set parent_id [$object parent_id]
set item_info [::$package_id item_ref -normalize_name false \
-use_package_path 1 \
-use_site_wide_pages true \
-default_lang [$object lang] \
-parent_id $parent_id \
$name]
#ns_log notice "*** resolve_form_name <$name> in $parent_id [$parent_id name] => $item_info"
set item_id [dict get $item_info item_id]
set form_name [dict get $item_info prefix]:[dict get $item_info stripped_name]
return [list form_id $item_id name $form_name]
}
Context instproc default_load_form_id {form_name} {
#:msg "resolving $form_name in state [:current_state] via default form loader"
set form_id 0
if {$form_name ne ""} {
set resolved [:resolve_form_name -object ${:object} $form_name]
set form_id [dict get $resolved form_id]
if {$form_id == 0} {
ns_log warning "could not resolve '$form_name' for ${:object}: $resolved"
}
#:msg ".... object ${:object} ==> id = $form_id"
}
return $form_id
}
::nsf::method::property Context default_load_form_id returns integer
Context instproc create_auto_form {object} {
#
# Create a form on the fly. The created form can be influenced by
# "auto_form_template" and "auto_form_constraints".
#
set vars [dict keys [$object set instance_attributes]]
set template [:auto_form_template]
if {$template ne ""} {
:log "USE autoform template"
} elseif {[llength $vars] == 0} {
#set template "AUTO form, no instance variables defined,
@_text@"
set template "@_text@"
} else {
set template "@[join $vars @,@]@
@_text@"
}
#:log "USE auto-form template=$template, vars=$vars \
# IA=[$object set instance_attributes], \
# V=[$object info vars] auto [:autoname]"
set package_id [$object package_id]
return [::xowiki::Form new -destroy_on_cleanup \
-package_id $package_id \
-parent_id [::$package_id folder_id] \
-name "Auto-Form" \
-anon_instances [:autoname] \
-form {} \
-text [list $template text/html] \
-form_constraints [:auto_form_constraints]]
}
Context instproc force_named_form {form_name} {
#
# By using this method in the "initialize" action, one can bypass
# the state specific forms and force a form to the certain name
#
set form_id [:default_load_form_id $form_name]
if {$form_id == 0} {
ns_log warning "use_named_form: could not locate form $form_name"
} else {
if {![nsf::is object ::${form_id}]} {
::xo::db::CrClass get_instance_from_db -item_id ${form_id}
}
set :form_id $form_id
}
}
Context instproc flush_form_object {} {
unset -nocomplain :form_obj
}
Context instproc form_object {object} {
set parent_id [$object parent_id]
# After this method is activated, the form object of the form of
# the current state is created and the instance variable form_id
# is set.
#
# Load the actual form only once for this context. We cache the
# object name of the form in the context.
#
if {[info exists :form_obj]} {
return ${:form_obj}
}
set package_id [$object package_id]
#
# We have to load the form, maybe via a form loader. If the
# form_loader is set nonempty and the method exists, then use the
# form loader instead of the plain lookup. In case the form_loader
# fails, it is supposed to return 0.
#
set loader [:form_loader]
#:msg form_loader=$loader
# TODO why no procsearch instead of "info methods"?
if {$loader eq "" || [:info methods $loader] eq ""} {
set form_id [:default_load_form_id [${:current_state} form]]
if {$form_id == 0} {
:log "=== NO default_load_form_id state ${:current_state} form <[${:current_state} form]>"
#
# When no form was found by the form loader ($form_id == 0) we
# create automatically a form.
#
set form_object [:create_auto_form $object]
:log "=== autoform $form_object"
}
} else {
#:msg "using custom form loader $loader for [:form]"
set form_object [:$loader [:form]]
}
#
# At this place, the variable "form_id" might contain an id
# (integer) or an object, provided by the custom file loader.
#
#:msg form_id=$form_id
if {![info exists form_object]
&& [string is integer -strict $form_id]
&& $form_id > 0
} {
# just load the object conditionally
if {![nsf::is object ::$form_id]} {
::xo::db::CrClass get_instance_from_db -item_id $form_id
}
set form_object ::$form_id
#:msg form_object=$form_object
}
if {[$form_object istype "::xowiki::Form"]} {
#
# The item returned from the form loader was a form,
# everything is fine.
#
#:msg form_object=$form_object-isForm
} elseif {[$form_object istype "::xowiki::FormPage"]} {
#
# We got a FormPage. This FormPage might be a pseudo form (a
# FormPage containing the property "form"). If not, add a "form"
# property from the rendered content.
#
#:msg form_object=$form_object-pseudoForm-with-form=[$form_object property form]
if {[$form_object property form] eq ""} {
#
# The FormPage contains no form, so try to provide one. We
# obtain the content by rendering the page_content. In some
# cases it might be more efficient to obtain the content
# from property "_text", but this might lead to unexpected
# cases where the formpage uses _text for partial
# information.
#
set text [$form_object render_content]
$form_object set_property -new 1 form "
[ns_quotehtml $errorInfo]" } ad_log error "--WF: evaluation $error\n$errorInfo" set next_state "" } on ok {result} { # # The action went ok. The call to "get_next_state" is here to # allow the developer to influence the outcome of # "get_next_state" by the activated method. # set next_state [$actionObj get_next_state] #:log "ACTIVATE ${:name} no error next-state <$next_state>" } return $next_state } WorkflowPage instproc get_form_data args { if {[:is_wf_instance]} { lassign [next] validation_errors category_ids if {$validation_errors == 0} { #:msg "validation ok" set ctx [::xowf::Context require [self]] set cc [${:package_id} context] foreach {name value} [$cc get_all_form_parameter] { if {[regexp {^__action_(.+)$} $name _ action]} { set actionObj [:get_action_obj -action $action] set next_state [:activate $ctx $action] #:log "after activate next_state=$next_state, current_state=[$ctx get_current_state], ${:instance_attributes}" if {$next_state ne ""} { if {[$actionObj exists assigned_to]} { :assignee [:get_assignee [$actionObj assigned_to]] } $ctx set_current_state $next_state } break } } } ns_log notice "===== get_form_data returns [list $validation_errors $category_ids]" return [list $validation_errors $category_ids] } else { next } } WorkflowPage instproc instantiated_form_fields {} { # Helper method to # - obtain the field_names from the current form, to # - create form_field instances from that and to # - provide the values from the instance attributes into it. lassign [:field_names_from_form] _ field_names set form_fields [:create_form_fields $field_names] :load_values_into_form_fields $form_fields return $form_fields } WorkflowPage ad_instproc solution_set {} { Compute solution set in form of attribute=value pairs based on "answer" attribute of form fields. } { set solutions [list] foreach f [:instantiated_form_fields] { if {![$f exists answer]} continue lappend solutions [$f name]=[$f answer] } return [join [lsort $solutions] ", "] } WorkflowPage ad_instproc answer_is_correct {} { Check, if answer is correct based on "answer" and "correct_when" attributes of form fields and provided user input. } { set correct 0 :log "WorkflowPage(${:name}).answer_is_correct autocorrect '[:get_from_template auto_correct]' -- [string is true -strict [:get_from_template auto_correct]]" if {[string is true -strict [:get_from_template auto_correct]]} { :log "==== answer_is_correct '[:instantiated_form_fields]'" foreach f [:instantiated_form_fields] { #:log [$f serialize] #:log "checking correctness [$f name] [$f info class] answer?[$f exists value] correct_when ?[$f exists correct_when]" if {[$f exists value]} { set r [$f answer_is_correct] #:log [$f serialize] if {$r != 1} { #:log [$f serialize] #:log "checking correctness [$f name] failed ([$f answer_is_correct])" set correct -1 break } set correct 1 } } } return $correct } WorkflowPage ad_instproc stats_record_count {name} { Record that the specified question was used. } { dict incr :__stats_count $name } WorkflowPage ad_instproc stats_record_detail { -label -value -name -correctly_answered:boolean } { } { dict set :__stats_label $name label $value $label if {[info exists :__stats_success] && [dict exists ${:__stats_success} $name $value]} { set details [dict get ${:__stats_success} $name $value] } else { set details "" } dict incr details $correctly_answered dict set :__stats_success $name $value $details } WorkflowPage instproc unset_temporary_instance_variables {} { # never save/cache the following variables array unset :__wfi array unset :__wf next } WorkflowPage instproc save_data args { if {[:is_wf_instance]} { # # update the state in the workflow instance # set ctx [::xowf::Context require [self]] set prev_state ${:state} set :state [$ctx get_current_state] if {$prev_state ne ${:state}} { # The form object in the cache is still that from the previous # state, make sure we flush it. $ctx flush_form_object } } next } WorkflowPage instproc save args { set r [next] :save_in_hstore return $r } WorkflowPage instproc save_new args { set r [next] :save_in_hstore return $r } WorkflowPage instproc hstore_attributes {} { # # We do not want to save the workflow definition in every workflow # instance. # return [dict remove ${:instance_attributes} workflow_definition] } WorkflowPage instproc save_in_hstore {} { # if {[::xo::dc has_hstore] && [${:package_id} get_parameter use_hstore 0]} { set hkey [::xowiki::hstore::dict_as_hkey [:hstore_attributes]] set revision_id ${:revision_id} xo::dc dml update_hstore "update xowiki_page_instance \ set hkey = '$hkey' \ where page_instance_id = :revision_id" } } WorkflowPage instproc wf_property {name {default ""}} { if {[info exists :__wf]} {set key :__wf($name)} else {set key :__wfi($name)} if {[info exists $key]} { return [set $key] } return $default } WorkflowPage instproc get_template_object {} { if {[:is_wf_instance]} { set key :__wfi(wf_form_id) if {![info exists $key]} { set ctx [::xowf::Context require [self]] set $key [$ctx form_object [self]] } set form_obj [set $key] if {![nsf::is object $form_obj]} { ad_log error "deprecated usage: method 'form_object' did NOT return an object. Will raise an error in the future" set form_id [string trimleft $form_obj :] set form_obj [::xo::db::CrClass get_instance_from_db -item_id $form_id] } return $form_obj } else { return [next] } } WorkflowPage instproc create-or-use_view {-package_id:required -parent_id:required name } { # the link should be able to view return_url and template_file return [::$package_id returnredirect [::$package_id pretty_link -parent_id $parent_id $lang:$stripped_name]] } WorkflowPage instproc www-create-or-use { {-parent_id:integer 0} {-view_method:wordchar edit} {-name ""} {-nls_language ""} } { #:msg "instance = [:is_wf_instance], wf=[:is_wf]" if {[:is_wf]} { # # In a first step, we call "allocate". Allocate is an Action # defined in a workflow, which is called *before* the workflow # instance is created. Via allocate, it is e.g. possible to # provide a computed name for the workflow instance from within # the workflow definition. # set ctx [::xowf::Context require [self]] set wfc [$ctx wf_container] :activate $ctx allocate # # After allocate, the payload might contain "name", "parent_id" # or "m". Using the payload dict has the advantage that it does # not touch the instance variables. # set payload [${wfc}::allocate payload] ns_log notice "AFTER ALLOCATE www-create-or-use <$payload>" set m "" set title "" foreach p {name title parent_id m} { if {[dict exists $payload $p]} { set $p [dict get $payload $p] } } set package ::${:package_id} if {$title ne ""} { ::xo::cc set_query_parameter title $title } # # If these values are not set, try to obtain it the old-fashioned way. # if {$parent_id == 0} { set parent_id [:query_parameter parent_id:int32 [$package folder_id]] } if {$name eq ""} { set name [:property name ""] } # # Check, if allocate has provided a name: # if {$name ne ""} { # Ok, a name was provided. Check if an instance with this name # exists in the current folder. set default_lang [:lang] $package get_lang_and_name -default_lang $default_lang -name $name lang stripped_name set id [::xo::db::CrClass lookup -name $lang:$stripped_name -parent_id $parent_id] #:log "after allocate lookup of $lang:$stripped_name returned $id, default-lang(${:name})=$default_lang [:nls_language]" if {$id != 0} { # # The instance exists already. Either use method "m" (if # provided) or redirect to the item. # if {$m eq ""} { return [$package returnredirect \ [export_vars -no_base_encode \ -base [$package pretty_link -parent_id $parent_id $lang:$stripped_name] \ {return_url template_file}]] } else { set item [::xo::db::CrClass get_instance_from_db -item_id $id] # missing: policy check. return [$item $m] } } else { if {$lang ne $default_lang} { set nls_language [:get_nls_language_from_lang $lang] } else { set nls_language [:nls_language] } #:msg "We want to create $lang:$stripped_name" set name $lang:$stripped_name } } } # method "m" is ignored, always edit next -parent_id $parent_id -view_method $view_method -name $name -nls_language $nls_language } WorkflowPage instproc initialize_loaded_object {} { next if {[:is_wf_instance]} { :initialize } } # -debug WorkflowPage instproc initialize {} { #:log START-initialize #:log "is_wf_instance [:is_wf_instance]" # # A fresh workflow page was created (called only once per # workflow page at initial creation) # if {[:is_wf_instance]} { # # Get context and call user defined "constructor" # # set the state to a well defined starting point if {${:state} eq ""} {set :state initial} set ctx [::xowf::Context require -new [self]] :activate -verbose false $ctx initialize # Ignore the returned next_state, since the initial state is # always set to the same value from the ctx (initial) #:msg "[self] is=${:instance_attributes}" } next #:log END-initialize } WorkflowPage instproc default_instance_attributes {} { # Provide the default list of instance attributes to derived # FormPages. if {[:is_wf]} { # # We have a workflow page. Get the initial state of the workflow # instance from the workflow. # set instance_attributes "" set ctx [::xowf::Context require [self]] foreach p [$ctx defined ::xowiki::formfield::FormField] { set name [$p name] set value [$p default] if {[::xo::cc exists_query_parameter $name]} { # # Never clobber instance attributes from query parameters # blindly. # #:msg "ignore $name" continue } if {[::xo::cc exists_query_parameter p.$name] && [$p exists allow_query_parameter]} { # # We allow the value to be taken from the query parameter. # set value [::xo::cc query_parameter p.$name] $p value $value $p validate $p } dict set instance_attributes $name $value set f($name) $p } ## save instance attributes #set instance_attributes [array get __ia] #:msg "[self] ${:name} setting default parameter" #:log ia=$instance_attributes,props=[$ctx defined Property] :state [$ctx get_current_state] #:msg "setting initial state to '[:state]'" return $instance_attributes } else { next } } WorkflowPage instproc constraints_as_array {c} { array set __c "" foreach name_and_spec $c { regexp {^([^:]+):(.*)$} $name_and_spec _ spec_name short_spec set __c($spec_name) $short_spec } return [array get __c] } WorkflowPage instproc merge_constraints {c1 args} { # Load into the base_constraints c1 the constraints from the argument list. # The first constraints have the lowest priority array set __c1 [:constraints_as_array $c1] foreach c2 $args { foreach {att value} [:constraints_as_array $c2] { set key __c1($att) if {[info exists $key]} {append $key ",$value"} else {set $key $value} } } set result [list] foreach {att value} [array get __c1] {lappend result $att:$value} return $result } WorkflowPage instproc wfi_merged_form_constraints {constraints_from_form} { set ctx [::xowf::Context require [self]] set wf_specific_constraints [${:page_template} property form_constraints] set m [:merge_constraints $wf_specific_constraints \ $constraints_from_form [$ctx get_form_constraints]] #:msg "merged:$m" return $m } WorkflowPage instproc wf_merged_form_constraints {constraints_from_form} { return $constraints_from_form #return [:merge_constraints $constraints_from_form [:property form_constraints]] } WorkflowPage instproc get_anon_instances {} { if {[:istype ::xowiki::FormPage] && [:is_wf_instance]} { # # In case, the workflow definition has the autoname variable set, # it has the highest weight of all other sources. # set wfc [[::xowf::Context require [self]] wf_container] if {[$wfc exists autoname]} { return [$wfc set autoname] } } next } WorkflowPage instproc get_form_constraints {{-trylocal false}} { if {[:istype ::xowiki::FormPage] && [:is_wf]} { #:msg "get_form_constraints is_wf" return [::xo::cc cache [list [self] wf_merged_form_constraints [next]]] } elseif {[:istype ::xowiki::FormPage] && [:is_wf_instance]} { #:msg "get_form_constraints is_wf_instance" return [::xo::cc cache [list [self] wfi_merged_form_constraints [next]]] } else { #:msg "get_form_constraints next" next } } WorkflowPage instproc visited_states {} { set item_id ${:item_id} foreach state [xo::dc list history { select DISTINCT state from xowiki_form_page p, cr_items i, cr_revisions r where i.item_id = :item_id and r.item_id = i.item_id and xowiki_form_page_id = r.revision_id}] { set visited($state) 1 } #:msg "visited states of item $item_id = [array names visited]" return [array names visited] } WorkflowPage ad_instproc get_revision_sets {-with_instance_attributes:switch} { Return a list of ns_sets containing revision_id, creation_date, creation_user, creation_ip, and state for the current workflow instance. } { set item_id ${:item_id} if {$with_instance_attributes} { set revision_sets [::xo::dc sets -prepare integer wf_revisions { SELECT revision_id, creation_date, last_modified, creation_user, creation_ip, state, assignee, instance_attributes FROM cr_revisions cr, acs_objects o, xowiki_form_page x, xowiki_page_instance pi WHERE cr.item_id = :item_id AND o.object_id = cr.revision_id AND x.xowiki_form_page_id = cr.revision_id AND pi.page_instance_id = cr.revision_id ORDER BY cr.revision_id ASC }] } else { set revision_sets [::xo::dc sets -prepare integer wf_revisions { SELECT revision_id, creation_date, last_modified, creation_user, creation_ip, state, assignee FROM cr_revisions cr, acs_objects o, xowiki_form_page x WHERE cr.item_id = :item_id AND o.object_id = cr.revision_id AND x.xowiki_form_page_id = cr.revision_id ORDER BY cr.revision_id ASC }] } return $revision_sets } WorkflowPage ad_instproc footer {} { Provide a tailored footer for workflow definition pages and workflow instance pages containing controls for instantiating forms or providing links to the workflow definition. } { if {[info exists :__no_form_page_footer]} { next } else { set parent_id [:parent_id] set form_item_id ${:page_template} #:msg "is wf page [:is_wf], is wf instance page [:is_wf_instance]" if {[:is_wf]} { # # page containing a work flow definition # #set ctx [::xowf::Context require [self]] set work_flow_form [::xo::db::CrClass get_instance_from_db -item_id $form_item_id] set work_flow_base [$work_flow_form pretty_link] set wf [self] set wf_base [$wf pretty_link] set button_objs [list] # create new workflow instance button with start form #if {[:parent_id] != [::${:package_id} folder_id]} { # set parent_id [:parent_id] #} set link [::${:package_id} make_link -link $wf_base $wf create-new parent_id return_url] lappend button_objs [::xowiki::includelet::form-menu-button-new new -volatile \ -parent_id $parent_id \ -form $wf -link $link] # list workflow instances button set obj [::xowiki::includelet::form-menu-button-wf-instances new -volatile \ -package_id ${:package_id} -parent_id $parent_id \ -base $wf_base -form $wf] if {[info exists return_url]} { $obj return_url $return_url } lappend button_objs $obj # work flow definition button set obj [::xowiki::includelet::form-menu-button-form new -volatile \ -package_id ${:package_id} -parent_id $parent_id \ -base $work_flow_base -form $work_flow_form] if {[info exists return_url]} {$obj return_url $return_url} lappend button_objs $obj # make menu return [:include [list form-menu -form_item_id ${:item_id} -button_objs $button_objs]] } elseif {[:is_wf_instance]} { # # work flow instance # set entry_form_item_id [:wf_property wf_form_id] set work_flow_form [::xo::db::CrClass get_instance_from_db -item_id $form_item_id] set work_flow_base [$work_flow_form pretty_link] set button_objs [list] #:msg entry_form_item_id=$entry_form_item_id-exists?=[nsf::is object $entry_form_item_id] # form definition button if {![nsf::is object $entry_form_item_id]} { # In case, the id is a form object, it is a dynamic form, # that we can't edit; therefore, we provide no link. # # Here, we have an id that we use for fetching... # set form [::xo::db::CrClass get_instance_from_db -item_id $entry_form_item_id] set base [$form pretty_link] set obj [::xowiki::includelet::form-menu-button-form new -volatile \ -package_id ${:package_id} -parent_id $parent_id \ -base $base -form $form] if {[info exists return_url]} { $obj return_url $return_url } lappend button_objs $obj } # # work flow definition button # set obj [::xowiki::includelet::form-menu-button-wf new -volatile \ -package_id ${:package_id} -parent_id $parent_id \ -base $work_flow_base -form $work_flow_form] if {[info exists return_url]} {$obj return_url $return_url} lappend button_objs $obj # make menu return [:include [list form-menu -form_item_id ${:page_template} -button_objs $button_objs]] } else { next } } } WorkflowPage ad_instproc call_action_foreach {-action:required {-attributes ""} page_names} { Call the specified action for each of the specified pages denoted by the list of page names } { foreach page_name $page_names { set page [${:package_id} get_page_from_name -parent_id [:parent_id] -name $page_name] if {$page ne ""} { $page call_action -action $action -attributes $attributes } else { ns_log notice "WF: could not call action $action, since $page_name in [:parent_id] failed" } } } WorkflowPage ad_instproc get_action_obj {-action:required} { Check if the action can be executed in the current state, and if so, return the action_obj. } { set ctx [::xowf::Context require [self]] # # First try to call the action in the current state # foreach a [$ctx get_actions] { if {[namespace tail $a] eq "$action"} { # In the current state, the specified action is allowed :log "--xowf action $action allowed -- name='${:name}'" return $a } } # # Some actions are state-safe, these can be called in every state # set actionObj [$ctx wf_definition_object $action] if {[nsf::is object $actionObj] && [$actionObj state_safe]} { # The action is defined as state-safe, so if can be called in every state :log "--xowf action $action state_safe -- name='${:name}'" return $actionObj } error "No state-safe action '$action' available in workflow instance [self] of \ [${:page_template} name] in state [$ctx get_current_state]\n\ Available actions: [[$ctx current_state] get_actions]" } WorkflowPage ad_instproc call_action {-action {-attributes {}}} { Call the specified action in the current workflow instance. The specified attributes are provided like form_parameters to the action of the workflow. } { if {![:is_wf_instance]} { error "Page [self] is not a Workflow Instance" } set actionObj [:get_action_obj -action $action] return [$actionObj invoke -attributes $attributes] } # # Interface to atjobs # WorkflowPage ad_instproc schedule_action { -time:required -party_id -action:required {-attributes {}} } { Schedule the specified action for the current workflow instance at the given time. The specified attributes are provided like form_parameters to the action of the workflow. @param time time when the atjob should be executed @param party_id party_id for the user executing the atjob @param action workflow action to be executed @param attributes arguments provided to the workflow action (attribute value pairs) } { if {![:is_wf_instance]} { error "Page [self] is not a Workflow Instance" } if {![info exists party_id]} { set party_id [::xo::cc user_id] } :schedule_job -time $time -party_id $party_id \ [list call_action -action $action -attributes $attributes] } WorkflowPage ad_instproc schedule_job {-time:required -party_id cmd} { Schedule the specified Tcl command for the current package instance at the given time. } { :log "-at $time" set j [::xowf::atjob new \ -time $time \ -party_id $party_id \ -cmd $cmd \ -url [:pretty_link] \ -object [self]] $j persist } ad_proc -private migrate_from_wf_current_state {} { # # Transform the former instance_attributes # "wf_current_state" to the xowiki::FormPage attribute "state", and # "wf_assignee" to the xowiki::FormPage attribute "assignee". # set count 0 foreach atts [xo::dc list_of_lists entries { select p.state,p.assignee,pi.instance_attributes,p.xowiki_form_page_id from xowiki_form_page p, xowiki_page_instance pi, cr_items i, cr_revisions r where r.item_id = i.item_id and p.xowiki_form_page_id = r.revision_id and pi.page_instance_id = r.revision_id }] { lassign $atts state assignee instance_attributes xowiki_form_page_id if {[dict exists $instance_attributes wf_current_state] && [dict get $instance_attributes wf_current_state] ne $state} { #Object msg "must update state $state for $xowiki_form_page_id to [dict get $instance_attributes wf_current_state]" xo::db dml update_state "update xowiki_form_page \ set state = '[dict get $instance_attributes wf_current_state]' where xowiki_form_page_id = :xowiki_form_page_id" incr count } if {[dict exists $instance_attributes wf_assignee] && [dict get $instance_attributes wf_assignee] ne $assignee } { #Object msg "must update assignee $assignee for $xowiki_form_page_id to [dict get $instance_attributes wf_assignee]" set wf_assignee [dict get $instance_attributes wf_assignee] xo::dc dml update_state "update xowiki_form_page set assignee = :wf_assignee \ where xowiki_form_page_id = :xowiki_form_page_id" incr count } } return $count } } # # In order to provide either a REST or a DAV interface, we have to # switch to basic authentication, since non-OpenACS software packages # don't know how to handle OpenACS cookies. The basic authentication # interface can be established in three steps: # # 1) Create a basic authentication handler, Choose a URL and # define optionally the package to be initialized: # Example: # ::xowf::dav create ::xowf::baHandler -url /handler -package ::xowf::Package # # 2) Make sure, the basic authentication handler is initialized during # startup. Write a *-init.tcl file containing a call to the # created handler. # Example: # ::xowf::baHandler register # # 3) Write procs with names such as GET, PUT, POST to handle # the requests. These procs overload the predefined behavior. # namespace eval ::xowf { # ::xo::dav should be probably changed to ::xo::ProtocolHandler for release ::xotcl::Class create ::xowf::dav -superclass ::xo::dav ::xowf::dav instproc get_package_id {} { if {${:uri} eq "/"} { set :wf "" # # Take the first package instance # set {:package_id} [lindex [$package instances] 0] ${:package} initialize -package_id ${:package_id} } else { set :wf /${:uri} ${:package} initialize -url ${:uri} } # :log package_id=${:package_id} return ${:package_id} } ::xowf::dav instproc call_action {-uri -action -attributes} { ${:package} initialize -url $uri set object_name [::$package_id set object] set page [::$package_id resolve_request -path $object_name method] if {$page eq ""} { set errorMsg cannot resolve '$object_name' in package [::$package_id package_url] ad_log error $errorMsg ns_return 406 text/plain "Error: $errorMsg" } elseif {[catch {set msg [$page call_action \ -action $action \ -attributes $attributes]} errorMsg]} { ad_log error "$uri $action $attributes resulted in $errorMsg" ns_return 406 text/plain "Error: $errorMsg\n" } else { ns_return 200 text/plain "Success: $msg\n" } } ::xowf::dav create ::xowf::dav-todo -url /dav-todo -package ::xowf::Package ::xowf::dav-todo proc GET {} { set p [::xowiki::Page new -package_id ${:package_id}] $p include [list wf-todo -ical 1 -workflow ${:wf}] #ns_return 200 text/plain GET-${:uri}-XXX-pid=${:package_id}-wf=${:wf}-[::xo::cc serialize] } # ::xowf::dav-todo proc GET {} { # set uri /xowf/153516 # set uri /xowf/18362 # set uri /xowf/18205 # :call_action -uri $uri -action work -attributes [list comment hello3 effort 4] # } proc include {wfName {vars ""}} { uplevel [::xowf::include_get -level 2 $wfName $vars] } ad_proc -private include_get {{-level 1} wfName {vars ""}} { Implement inclusion of workflow definitions. } { if {![string match "/packages/*/lib/*" $wfName]} { error "path leading to workflow name must look like /packages/*/lib/*" } set fname $::acs::rootdir/$wfName if {![ad_file readable $fname]} { error "file '$fname' not found" } # # Tell the caller, what files were included in the thread # invocation. It would be nicer to have this more OO, such we can # avoid the global variable ::__xowf_depends. # lappend ::__xowf_depends $fname [ad_file mtime $fname] set f [open $fname]; set wfDefinition [read $f]; close $f #::xotcl::Object log "INCLUDE $wfName [list $vars]" if {[llength $vars] > 0} { foreach var $vars { lappend substMap @$var@ [uplevel $level [list set $var]] } set wfDefinition [string map $substMap $wfDefinition] } #::xotcl::Object log "AFTER SUBST $wfName [list $vars]\n$wfDefinition" return [list eval $wfDefinition] } } namespace eval ::xowiki { ::xowiki::MenuBar instproc config=xowf { {-bind_vars {}} -current_page:required -package_id:required -folder_link:required -return_url } { :config=default \ -bind_vars $bind_vars \ -current_page $current_page \ -package_id $package_id \ -folder_link $folder_link \ -return_url $return_url return { {entry -name New.Extra.Workflow -form en:Workflow.form} } } } ::xo::library source_dependent # # Local variables: # mode: tcl # tcl-indent-level: 2 # indent-tabs-mode: nil # End: