Реализовать возможность ввода 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. 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), 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).
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.
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 == 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. 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 == Compatibility
This plug-in is compatible with Redmine 1.4.x, 2.0.x, 2.1.x This plug-in is compatible with Redmine 1.4.x, 2.0.x, 2.1.x
== Changelog == 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 [0.0.1] Initial commit

View File

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

View File

@ -2,8 +2,18 @@
<div class="box tabular"> <div class="box tabular">
<p><%= f.text_field :name, :required => true, :size => 50 %></p> <p><%= f.text_field :name, :required => true, :size => 50 %></p>
<p><%= f.text_area :description, :cols => 80, :rows => 5 %></p> <p><%= f.text_area :description, :cols => 80, :rows => 5 %></p>
<p><%= f.text_area :script, :cols => 80, :rows => 20, :wrap => 'off', :required => true %> <p><label><%= l(:label_workflow_scripts) %></label>
<em class="info"><%= l(:text_custom_workflow_script_note) %></em> <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> </p>
</div> </div>

View File

@ -1,11 +1,9 @@
#admin-menu a.custom-workflows { #admin-menu a.custom-workflows {
background-image: url(../../../images/ticket_go.png); background-image: url(../../../images/ticket_go.png);
} }
table.list.custom-workflows td { table.list.custom-workflows td { vertical-align: middle; }
vertical-align: middle;
}
#custom_workflow_description, #custom_workflow_script { #custom_workflow_description { width: 99%; }
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_plural: "Custom workflows"
label_custom_workflow_new: "Create a custom workflow" 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_is_enabled: "Enabled"
field_custom_workflow: field_custom_workflow:
script: "Workflow script" script: "Workflow script"
activerecord: activerecord:
errors: errors:
messages: messages:
@ -20,4 +21,6 @@ en:
new_status_invalid: "transition from '%{old_status}' to '%{new_status}' is prohibited" new_status_invalid: "transition from '%{old_status}' to '%{new_status}' is prohibited"
text_select_project_custom_workflows: Select project custom workflows 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_plural: "Пользовательские рабочие процессы"
label_custom_workflow_new: "Новый процесс" label_custom_workflow_new: "Новый процесс"
field_script: "Скрипт управления" label_workflow_scripts: "Сценарии"
field_after_save: "Сценарий (после сохранения задачи)"
field_before_save: "Сценарий (перед сохранением задачи)"
field_is_enabled: "Разрешено" field_is_enabled: "Разрешено"
field_custom_workflow: field_custom_workflow:
script: "Скрипт управления" script: "Сценарий"
activerecord: activerecord:
errors: errors:
messages: messages:
invalid_script: "содержит ошибку: %{error}" invalid_script: "содержит ошибку: %{error}"
custom_workflow_error: "Ошибка в рабочем процессе (обратитесь к администратору)" custom_workflow_error: "Ошибка в сценарии рабочего процесса (обратитесь к администратору)"
new_status_invalid: "- переход от '%{old_status}' к '%{new_status}' невозможен" new_status_invalid: "- переход от '%{old_status}' к '%{new_status}' невозможен"
text_select_project_custom_workflows: Выберите процессы для данного проекта 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 class CreateExampleWorkflow < ActiveRecord::Migration
def self.up def self.up
CustomWorkflow.create(:name => "Duration/Done Ratio/Status correlation", :description => <<EOD, :script => <<EOS) 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) def self.included(base)
base.send(:include, InstanceMethods) base.send(:include, InstanceMethods)
base.class_eval do base.class_eval do
before_save :custom_workflow_eval before_save :before_save_custom_workflows
after_save :after_save_custom_workflows
validate :validate_status validate :validate_status
end end
end end
@ -22,22 +23,35 @@ module RedmineCustomWorkflows
end end
end end
def custom_workflow_eval def run_custom_workflows(on)
return true unless project && project.module_enabled?(:custom_workflows_module) 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| project.custom_workflows.each do |workflow|
begin begin
workflow.eval_script(:issue => self) Rails.logger.info "== Running #{on} custom workflow \"#{workflow.name}\""
instance_eval(workflow.read_attribute(on))
rescue WorkflowError => e rescue WorkflowError => e
Rails.logger.info "== User workflow error: #{e.message}"
errors.add :base, e.error errors.add :base, e.error
return false return false
rescue Exception => e 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 errors.add :base, :custom_workflow_error
return false return false
end end
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 end
end end