Реализовать возможность ввода after_save скрипта для рабочего процесса (#8671)

This commit is contained in:
Anton Argirov 2012-09-08 19:15:34 +07:00
parent 6df2bafc72
commit ae48e0be6c
10 changed files with 153 additions and 33 deletions

View File

@ -29,8 +29,9 @@ First, you need to define your own custom workflow(s). We already included one,
Go to the *Administration* section, then select <b>Custom workflows</b>. A list of defined workflows will appear. Here you can create new workflow, update, reorder and delete existing workflows. The order of workflows specifies the order in which workflow scripts will be executed.
Then click the <b>Create a custom workflow</b> button. Enter a short name, full description and script itself. The script entered will be executed before saving issues (on before_save callback),
so you can use <b>@issue</b> variable to access the issue properties and methods. You can also raise exceptions by <b>raise WorkflowError, "Your message"</b>. If you change some properties of the issue during the script execution, it will be revalidated then and additional validation errors can appear.
Then click the <b>Create a custom workflow</b> button. Enter a short name, full description and script itself. Below you will see two textareas. Fill one or both textareas by Ruby-language scripts that will be executed before and after saving the issue (on before_save and after_save callbacks respectively).
Both scripts are executed in the context of the issue. So access properties and methods of the issue directly (or through keyword "self"). You can also raise exceptions by <b>raise WorkflowError, "Your message"</b>. If you change some properties of the issue before saving it, it will be revalidated then and additional validation errors can appear.
== Enabling workflows for projects
@ -38,10 +39,57 @@ After you defined your custom workflow(s), you need to enable it. Open <b>Projec
Now go to the *Issues* and test it.
== Duration/Done Ratio/Status correlation example
Fill the "before save" script with:
if done_ratio_changed?
if done_ratio==100 && status_id==2
self.status_id=3
elsif [1,3,4].include?(status_id) && done_ratio<100
self.status_id=2
end
end
if status_id_changed?
if status_id==2
self.start_date ||= Time.now
end
if status_id==3
self.done_ratio = 100
self.start_date ||= created_on
self.due_date ||= Time.now
end
end
== Example of creating subtask if the issue's status has changed.
Fill the "before save" script with:
@need_create = status_id_changed? && !new_record?
Fill the "after save" script with:
if @need_create
issue = Issue.new(
:author => User.current,
:project => project,
:tracker => tracker,
:assigned_to => author,
:parent_issue_id => id,
:subject => "Subtask",
:description => "Description")
issue.save!
end
Do not forget to check whether issue is just created. Here we create the new issue and newly created issue will also be passed to this script on save. So without check, it will create another sub-issue. And etc. Thus it will fall into infinite recursion.
== Compatibility
This plug-in is compatible with Redmine 1.4.x, 2.0.x, 2.1.x
== Changelog
[0.0.2] Added ability to define after_save script along with before_save, improved logging, changed context of executing script to the issue.
[0.0.1] Initial commit

View File

@ -11,22 +11,24 @@ class CustomWorkflow < ActiveRecord::Base
acts_as_list
default_scope :order => 'position ASC'
validates_presence_of :script
validates_presence_of :name
validates_uniqueness_of :name, :case_sensitive => false
validate :validate_syntax
def eval_script(context)
context.each { |k, v| instance_variable_set ("@#{k}").to_sym, v }
eval(script)
end
def validate_syntax
issue = Issue.new
issue.send :instance_variable_set, :@issue, issue # compatibility with 0.0.1
begin
eval_script(:issue => Issue.new)
issue.instance_eval(before_save)
rescue WorkflowError => e
rescue Exception => e
errors.add :script, :invalid_script, :error => e
errors.add :before_save, :invalid_script, :error => e
end
begin
issue.instance_eval(after_save)
rescue WorkflowError => e
rescue Exception => e
errors.add :after_save, :invalid_script, :error => e
end
end

View File

@ -2,8 +2,18 @@
<div class="box tabular">
<p><%= f.text_field :name, :required => true, :size => 50 %></p>
<p><%= f.text_area :description, :cols => 80, :rows => 5 %></p>
<p><%= f.text_area :script, :cols => 80, :rows => 20, :wrap => 'off', :required => true %>
<em class="info"><%= l(:text_custom_workflow_script_note) %></em>
<p><label><%= l(:label_workflow_scripts) %></label>
<span class="splitcontentleft">
<%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off', :no_label => true %>
<em class="info"><%= l(:text_custom_workflow_before_save_note) %></em>
</span>
<span class="splitcontentright">
<%= f.text_area :after_save, :cols => 40, :rows => 20, :wrap => 'off', :no_label => true %>
<em class="info"><%= l(:text_custom_workflow_after_save_note) %></em>
</span>
</p>
<p>
<em class="info"><%= l(:text_custom_workflow_general_note) %></em>
</p>
</div>

View File

@ -2,10 +2,8 @@
background-image: url(../../../images/ticket_go.png);
}
table.list.custom-workflows td {
vertical-align: middle;
}
table.list.custom-workflows td { vertical-align: middle; }
#custom_workflow_description, #custom_workflow_script {
width: 99%;
}
#custom_workflow_description { width: 99%; }
#custom_workflow_before_save, #custom_workflow_after_save { width: 98%; font-size: 11px; }

View File

@ -5,13 +5,14 @@ en:
label_custom_workflow_plural: "Custom workflows"
label_custom_workflow_new: "Create a custom workflow"
field_script: "Workflow script"
label_workflow_scripts: "Workflow scripts"
field_after_save: "Workflow script (after save)"
field_before_save: "Workflow script (before save)"
field_is_enabled: "Enabled"
field_custom_workflow:
script: "Workflow script"
activerecord:
errors:
messages:
@ -20,4 +21,6 @@ en:
new_status_invalid: "transition from '%{old_status}' to '%{new_status}' is prohibited"
text_select_project_custom_workflows: Select project custom workflows
text_custom_workflow_script_note: Use the variable @issue for an access to the issue. To finish the process with an error, use raise WorkflowError, "Message to user"
text_custom_workflow_before_save_note: This script will run BEFORE saving the issue. You can change properties of the issues here. Do not create or update related issues in this script. To finish with error, use raise WorkflowError, "Message to user".
text_custom_workflow_after_save_note: This script will run AFTER saving the issue. You can update or create related issues here. Note that this script will be also executed for the newly created issues. So make appropriate checks to prevent infinite recursion.
text_custom_workflow_general_note: Both scripts are executed in the context of the issue like ordinary before_save and after_save callbacks. So use methods and properties of the issue directly (or through "self"). Instance variables (@variable) are also allowed and may be used if needed.

View File

@ -5,19 +5,22 @@ ru:
label_custom_workflow_plural: "Пользовательские рабочие процессы"
label_custom_workflow_new: "Новый процесс"
field_script: "Скрипт управления"
label_workflow_scripts: "Сценарии"
field_after_save: "Сценарий (после сохранения задачи)"
field_before_save: "Сценарий (перед сохранением задачи)"
field_is_enabled: "Разрешено"
field_custom_workflow:
script: "Скрипт управления"
script: "Сценарий"
activerecord:
errors:
messages:
invalid_script: "содержит ошибку: %{error}"
custom_workflow_error: "Ошибка в рабочем процессе (обратитесь к администратору)"
custom_workflow_error: "Ошибка в сценарии рабочего процесса (обратитесь к администратору)"
new_status_invalid: "- переход от '%{old_status}' к '%{new_status}' невозможен"
text_select_project_custom_workflows: Выберите процессы для данного проекта
text_custom_workflow_script_note: Используйте переменную @issue для доступа к задаче. Для завершения процесса с ошибкой используйте исключение WorkflowError.
text_custom_workflow_before_save_note: Этот сценарий будет выполнен ПЕРЕД сохранением задачи. Здесь вы можете изменять свойства задачи. Не создавайте и не обновляйте связанные задачи в этом сценарии. Чтобы завершить сценарий с произвольной ошибкой, используйте raise WorkflowError, "Message to user".
text_custom_workflow_after_save_note: Этот сценарий будет выполнен ПОСЛЕ сохранения задачи. Вы можете обновлять и создавать задачи (в том числе и связанные задачи) здесь. Обратите внимание, что данный сценарий будет также выполняться и для вновь создаваемых задач. Поэтому используйте дополнительные проверки, чтобы избежать бесконечной рекурсии.
text_custom_workflow_general_note: Оба сценария исполняются в контексте задачи, как и обычные обратные вызовы before_save и after_save. Поэтому используйте методы и свойства задачи напрямую или через ключевое слово self.

View File

@ -1,3 +1,4 @@
class CreateExampleWorkflow < ActiveRecord::Migration
def self.up
CustomWorkflow.create(:name => "Duration/Done Ratio/Status correlation", :description => <<EOD, :script => <<EOS)

View File

@ -0,0 +1,11 @@
class AddAfterSaveToCustomWorkflows < ActiveRecord::Migration
def self.up
rename_column :custom_workflows, :script, :before_save
change_column :custom_workflows, :before_save, :text, :null => false, :default => ""
add_column :custom_workflows, :after_save, :text, :null => false, :default => ""
end
def self.down
remove_column :custom_workflows, :after_save
rename_column :custom_workflows, :before_save, :script
end
end

View File

@ -0,0 +1,30 @@
class FixExampleWorkflow < ActiveRecord::Migration
def self.up
if workflow = CustomWorkflow.find_by_name("Duration/Done Ratio/Status correlation")
workflow.before_save = <<EOS
if done_ratio_changed?
if done_ratio==100 && status_id==2
self.status_id=3
elsif [1,3,4].include?(status_id) && done_ratio<100
self.status_id=2
end
end
if status_id_changed?
if status_id==2
self.start_date ||= Time.now
end
if status_id==3
self.done_ratio = 100
self.start_date ||= created_on
self.due_date ||= Time.now
end
end
EOS
workflow.save
end
end
def self.down
end
end

View File

@ -5,7 +5,8 @@ module RedmineCustomWorkflows
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
before_save :custom_workflow_eval
before_save :before_save_custom_workflows
after_save :after_save_custom_workflows
validate :validate_status
end
end
@ -22,22 +23,35 @@ module RedmineCustomWorkflows
end
end
def custom_workflow_eval
def run_custom_workflows(on)
return true unless project && project.module_enabled?(:custom_workflows_module)
saved_attributes = attributes.dup
@issue = self # compatibility with 0.0.1
Rails.logger.info "= Running #{on} custom workflows for issue \"#{subject}\" (##{id})"
project.custom_workflows.each do |workflow|
begin
workflow.eval_script(:issue => self)
Rails.logger.info "== Running #{on} custom workflow \"#{workflow.name}\""
instance_eval(workflow.read_attribute(on))
rescue WorkflowError => e
Rails.logger.info "== User workflow error: #{e.message}"
errors.add :base, e.error
return false
rescue Exception => e
Rails.logger.warn e
Rails.logger.error "== Custom workflow exception: #{e.message}\n #{e.backtrace.join("\n ")}"
errors.add :base, :custom_workflow_error
return false
end
end
saved_attributes == attributes || valid?
Rails.logger.info "= Finished running #{on} custom workflows for issue \"#{subject}\" (##{id})."
true
end
def before_save_custom_workflows
saved_attributes = attributes.dup
run_custom_workflows(:before_save) && (saved_attributes == attributes || valid?)
end
def after_save_custom_workflows
run_custom_workflows(:after_save)
end
end
end