Allow custom workflows to be enabled globally for all projects (#8750)

This commit is contained in:
Anton Argirov 2012-10-08 14:19:46 +07:00
parent d7f38170cc
commit f2fbce3b10
14 changed files with 154 additions and 36 deletions

View File

@ -35,7 +35,9 @@ Both scripts are executed in the context of the issue. So access properties and
== Enabling workflows for projects == Enabling workflows for projects
After you defined your custom workflow(s), you need to enable it. Open <b>Project settings</b>. Go to the <b>Enabled modules</b>. Enable the <b>Custom workflows</b> module there and click *Save*. Then go to the <b>Custom workflows</b> tab of the project settings and enable workflow(s) you need for this project. After you defined your custom workflow(s), you need to enable it for particular project(s). There are two ways of doing this.
* While editing existing or creating new custom workflow;
* In project settings (if the user has appropriate permission). Open <b>Project settings</b>. Go to the <b>Custom workflows</b> tab of the project settings and enable workflow(s) you need for this project.
Now go to the *Issues* and test it. Now go to the *Issues* and test it.
@ -91,6 +93,8 @@ This plug-in is compatible with Redmine 1.2.x, 1.3.x, 1.4.x, 2.0.x, 2.1.x
== Changelog == Changelog
[0.0.4] * Added ability to enable workflows globally for all projects. No need to enable 'Custom workflows' project module anymore. Just go to the 'Administration' -> 'Custom workflows' section and enable or disable your workflows in one place.
* Fixed bug with 'Status transition prohibited' when updating the issue status by the repository commit
[0.0.3] Compatibility with 1.2.x, 1.3.x [0.0.3] Compatibility with 1.2.x, 1.3.x
[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.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

@ -6,7 +6,7 @@ class CustomWorkflowsController < ApplicationController
before_filter :find_workflow, :only => [:show, :edit, :update, :destroy] before_filter :find_workflow, :only => [:show, :edit, :update, :destroy]
def index def index
@workflows = CustomWorkflow.all @workflows = CustomWorkflow.find(:all, :include => [:projects])
respond_to do |format| respond_to do |format|
format.html format.html
end end

View File

@ -7,6 +7,8 @@ class WorkflowError < StandardError
end end
class CustomWorkflow < ActiveRecord::Base class CustomWorkflow < ActiveRecord::Base
unloadable
has_and_belongs_to_many :projects has_and_belongs_to_many :projects
acts_as_list acts_as_list
@ -15,6 +17,12 @@ class CustomWorkflow < ActiveRecord::Base
validates_uniqueness_of :name, :case_sensitive => false validates_uniqueness_of :name, :case_sensitive => false
validate :validate_syntax validate :validate_syntax
if Gem::Version.new(Rails::VERSION::STRING) < Gem::Version.new("3.1.0")
named_scope :for_all, :conditions => {:is_for_all => true}
else
scope :for_all, where(:is_for_all => true)
end
def validate_syntax def validate_syntax
issue = Issue.new issue = Issue.new
issue.send :instance_variable_set, :@issue, issue # compatibility with 0.0.1 issue.send :instance_variable_set, :@issue, issue # compatibility with 0.0.1
@ -32,6 +40,10 @@ class CustomWorkflow < ActiveRecord::Base
end end
end end
def <=>(other)
self.position <=> other.position
end
def to_s def to_s
name name
end end

View File

@ -1,24 +1,53 @@
<div class="box tabular"> <div class="splitcontent">
<div class="splitcontentleft box">
<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 => 40, :rows => 5 %></p>
<p><label><%= l(:label_workflow_scripts) %></label> <p><label><%= f.check_box :is_for_all, :onclick => "checkAndDisable('custom_workflow_enabled_projects', this.checked);", :no_label => true %> <%= l(:field_enabled_for_all_projects) %></label></p>
<span class="splitcontentleft"> </div>
<%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off', :no_label => true %>
<div class="splitcontentright">
<fieldset class="box" id="custom_workflow_enabled_projects">
<legend><%= l(:label_enabled_projects) %></legend>
<%= custom_workflows_render_nested_projects(Project.visible.active) do |p|
content_tag('label', check_box_tag('custom_workflow[project_ids][]', p.id, @workflow.project_ids.include?(p.id) || @workflow.is_for_all?,
:id => nil, :disabled => @workflow.is_for_all?) + ' ' + h(p), :class => 'block')
end %>
<%= hidden_field_tag('custom_workflow[project_ids][]', '', :id => nil) %>
<p><%= check_all_links 'custom_workflow_enabled_projects' %></p>
</fieldset>
</div>
</div>
<div style="clear: left;"></div>
<fieldset class="box">
<legend><%= l(:label_workflow_scripts) %></legend>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off' %>
<em class="info"><%= l(:text_custom_workflow_before_save_note) %></em> <em class="info"><%= l(:text_custom_workflow_before_save_note) %></em>
</span> </div>
<span class="splitcontentright"> <div class="splitcontentright">
<%= f.text_area :after_save, :cols => 40, :rows => 20, :wrap => 'off', :no_label => true %> <%= f.text_area :after_save, :cols => 40, :rows => 20, :wrap => 'off' %>
<em class="info"><%= l(:text_custom_workflow_after_save_note) %></em> <em class="info"><%= l(:text_custom_workflow_after_save_note) %></em>
</span> </div>
</p> </div>
<div style="clear: left;"></div>
<p> <p>
<em class="info"><%= l(:text_custom_workflow_general_note) %></em> <em class="info"><%= l(:text_custom_workflow_general_note) %></em>
</p> </p>
</div> </fieldset>
<script type="text/javascript"> <script type="text/javascript">
jQuery('#custom_workflow_script').taboverride(2, true); jQuery('#custom_workflow_script').taboverride(2, true);
function checkAndDisable(id, checked) {
if (checked) {
jQuery('#'+id).find('input[type=checkbox]').attr('checked', true).attr('disabled', true);
} else {
jQuery('#'+id).find('input[type=checkbox]').removeAttr('checked').removeAttr('disabled');
}
}
</script> </script>
<%= wikitoolbar_for :custom_workflow_description %> <%= wikitoolbar_for :custom_workflow_description %>

View File

@ -12,6 +12,7 @@
<tr> <tr>
<th><%=l(:field_name)%></th> <th><%=l(:field_name)%></th>
<th><%=l(:field_description)%></th> <th><%=l(:field_description)%></th>
<th><%=l(:label_project_plural)%></th>
<th style="width:15%;"><%= l(:button_sort) %></th> <th style="width:15%;"><%= l(:button_sort) %></th>
<th style="width:10%;"></th> <th style="width:10%;"></th>
</tr> </tr>
@ -21,6 +22,15 @@
<tr class="<%= cycle("odd", "even") %>"> <tr class="<%= cycle("odd", "even") %>">
<td class="name"><%= link_to(workflow.name, edit_custom_workflow_path(workflow)) %></td> <td class="name"><%= link_to(workflow.name, edit_custom_workflow_path(workflow)) %></td>
<td><%= textilizable(workflow.description) %></td> <td><%= textilizable(workflow.description) %></td>
<td align="center">
<% if workflow.is_for_all? %>
<%= l(:field_enabled_for_all_projects) %>
<% elsif workflow.projects.empty? %>
<%= l(:text_no_enabled_projects) %>
<% else %>
<%= workflow.projects.map(&:name).join(", ") %>
<% end %>
</td>
<td align="center"><%= reorder_links("custom_workflow", {:action => 'update', :id => workflow}) %></td> <td align="center"><%= reorder_links("custom_workflow", {:action => 'update', :id => workflow}) %></td>
<td class="buttons"> <td class="buttons">
<%= link_to(l(:button_delete), workflow, :class => 'icon icon-del', :data => {:confirm => l(:text_are_you_sure)}, :confirm => l(:text_are_you_sure), :method => :delete) %> <%= link_to(l(:button_delete), workflow, :class => 'icon icon-del', :data => {:confirm => l(:text_are_you_sure)}, :confirm => l(:text_are_you_sure), :method => :delete) %>

View File

@ -6,7 +6,7 @@
<% if CustomWorkflow.exists? %> <% if CustomWorkflow.exists? %>
<dl> <dl>
<% CustomWorkflow.all.each do |w| %> <% CustomWorkflow.all.each do |w| %>
<dt><label><%= check_box_tag 'project[custom_workflow_ids][]', w.id, @project.custom_workflow_ids.include?(w.id) %> <%= w.name %></label></dt> <dt><label><%= check_box_tag 'project[custom_workflow_ids][]', w.id, @project.custom_workflow_ids.include?(w.id) || w.is_for_all?, :disabled => w.is_for_all? %> <%= w.name %></label></dt>
<dd><em><%= textilizable(w.description) %></em></dd> <dd><em><%= textilizable(w.description) %></em></dd>
<% end %> <% end %>
</dl> </dl>

View File

@ -4,6 +4,8 @@
table.list.custom-workflows td { vertical-align: middle; } table.list.custom-workflows td { vertical-align: middle; }
#custom_workflow_description { width: 99%; } #custom_workflow_description, #custom_workflow_name { width: 98%; }
#custom_workflow_before_save, #custom_workflow_after_save { width: 98%; font-size: 11px; } #custom_workflow_before_save, #custom_workflow_after_save { width: 98%; font-size: 11px; }
#custom_workflow_enabled_projects ul { max-height: 200px; overflow-y: auto; }

View File

@ -4,11 +4,13 @@ en:
label_custom_workflow: "Custom workflows" label_custom_workflow: "Custom workflows"
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"
label_workflow_scripts: "Workflow scripts" label_workflow_scripts: "Workflow scripts"
field_after_save: "Workflow script (after save)" label_enabled_projects: "Enabled for project(s)"
field_before_save: "Workflow script (before save)"
field_after_save: "Workflow script executable after saving the issue"
field_before_save: "Workflow script executable before saving the issue"
field_is_enabled: "Enabled" field_is_enabled: "Enabled"
field_enabled_for_all_projects: "Enabled for all projects"
field_custom_workflow: field_custom_workflow:
script: "Workflow script" script: "Workflow script"
@ -21,6 +23,7 @@ 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_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_before_save_note: 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_after_save_note: 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. 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.
text_no_enabled_projects: No projects

View File

@ -4,11 +4,13 @@ ru:
label_custom_workflow: "Пользовательский рабочий процесс" label_custom_workflow: "Пользовательский рабочий процесс"
label_custom_workflow_plural: "Пользовательские рабочие процессы" label_custom_workflow_plural: "Пользовательские рабочие процессы"
label_custom_workflow_new: "Новый процесс" label_custom_workflow_new: "Новый процесс"
label_workflow_scripts: "Сценарии" label_workflow_scripts: "Сценарии"
field_after_save: "Сценарий (после сохранения задачи)" label_enabled_projects: "Разрешен в проектах"
field_before_save: "Сценарий (перед сохранением задачи)"
field_after_save: "Сценарий выполняемый после сохранения задачи"
field_before_save: "Сценарий выполняемый перед сохранением задачи"
field_is_enabled: "Разрешено" field_is_enabled: "Разрешено"
field_enabled_for_all_projects: "Разрешен для всех проектов"
field_custom_workflow: field_custom_workflow:
script: "Сценарий" script: "Сценарий"
@ -21,6 +23,7 @@ ru:
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_before_save_note: Этот сценарий будет выполнен ПЕРЕД сохранением задачи. Здесь вы можете изменять свойства задачи. Не создавайте и не обновляйте связанные задачи в этом сценарии. Чтобы завершить сценарий с произвольной ошибкой, используйте raise WorkflowError, "Message to user". text_custom_workflow_before_save_note: Здесь вы можете изменять свойства задачи. Не создавайте и не обновляйте связанные задачи в этом сценарии. Чтобы завершить сценарий с произвольной ошибкой, используйте raise WorkflowError, "Message to user".
text_custom_workflow_after_save_note: Этот сценарий будет выполнен ПОСЛЕ сохранения задачи. Вы можете обновлять и создавать задачи (в том числе и связанные задачи) здесь. Обратите внимание, что данный сценарий будет также выполняться и для вновь создаваемых задач. Поэтому используйте дополнительные проверки, чтобы избежать бесконечной рекурсии. text_custom_workflow_after_save_note: Вы можете обновлять и создавать задачи (в том числе и связанные задачи) здесь. Обратите внимание, что данный сценарий будет также выполняться и для вновь создаваемых задач. Поэтому используйте дополнительные проверки, чтобы избежать бесконечной рекурсии.
text_custom_workflow_general_note: Оба сценария исполняются в контексте задачи, как и обычные обратные вызовы before_save и after_save. Поэтому используйте методы и свойства задачи напрямую или через ключевое слово self. text_custom_workflow_general_note: Оба сценария исполняются в контексте задачи, как и обычные обратные вызовы before_save и after_save. Поэтому используйте методы и свойства задачи напрямую или через ключевое слово self.
text_no_enabled_projects: Нет проектов

View File

@ -0,0 +1,8 @@
class AddIsForAllToCustomWorkflows < ActiveRecord::Migration
def self.up
add_column :custom_workflows, :is_for_all, :boolean, :null => false, :default => false
end
def self.down
remove_column :custom_workflows, :is_for_all
end
end

14
init.rb
View File

@ -2,15 +2,18 @@ require 'redmine'
require 'redmine_custom_workflows/hooks' require 'redmine_custom_workflows/hooks'
to_prepare = Proc.new do to_prepare = Proc.new do
unless Project.included_modules.include?(RedmineCustomWorkflows::ProjectPatch) unless Project.include?(RedmineCustomWorkflows::ProjectPatch)
Project.send(:include, RedmineCustomWorkflows::ProjectPatch) Project.send(:include, RedmineCustomWorkflows::ProjectPatch)
end end
unless ProjectsHelper.included_modules.include?(RedmineCustomWorkflows::ProjectsHelperPatch) unless ProjectsHelper.include?(RedmineCustomWorkflows::ProjectsHelperPatch)
ProjectsHelper.send(:include, RedmineCustomWorkflows::ProjectsHelperPatch) ProjectsHelper.send(:include, RedmineCustomWorkflows::ProjectsHelperPatch)
end end
unless Issue.included_modules.include?(RedmineCustomWorkflows::IssuePatch) unless Issue.include?(RedmineCustomWorkflows::IssuePatch)
Issue.send(:include, RedmineCustomWorkflows::IssuePatch) Issue.send(:include, RedmineCustomWorkflows::IssuePatch)
end end
unless ActionView::Base.include?(RedmineCustomWorkflows::Helper)
ActionView::Base.send(:include, RedmineCustomWorkflows::Helper)
end
end end
if Redmine::VERSION::MAJOR >= 2 if Redmine::VERSION::MAJOR >= 2
@ -24,13 +27,10 @@ Redmine::Plugin.register :redmine_custom_workflows do
name 'Redmine Custom Workflow plugin' name 'Redmine Custom Workflow plugin'
author 'Anton Argirov' author 'Anton Argirov'
description 'Allows to create custom workflows for issues, defined in the plain Ruby language' description 'Allows to create custom workflows for issues, defined in the plain Ruby language'
version '0.0.3' version '0.0.4'
url 'http://redmine.academ.org' url 'http://redmine.academ.org'
menu :admin_menu, :custom_workflows, {:controller => 'custom_workflows', :action => 'index'}, :caption => :label_custom_workflow_plural menu :admin_menu, :custom_workflows, {:controller => 'custom_workflows', :action => 'index'}, :caption => :label_custom_workflow_plural
project_module :custom_workflows_module do
permission :manage_project_workflow, {}, :require => :member permission :manage_project_workflow, {}, :require => :member
end end
end

View File

@ -0,0 +1,38 @@
module RedmineCustomWorkflows
module Helper
unloadable
# Renders a tree of projects as a nested set of unordered lists
# The given collection may be a subset of the whole project tree
# (eg. some intermediate nodes are private and can not be seen)
def custom_workflows_render_nested_projects(projects)
s = ''
if projects.any?
ancestors = []
original_project = @project
projects.sort_by(&:lft).each do |project|
# set the project environment to please macros.
@project = project
if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
else
ancestors.pop
s << "</li>"
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
s << "</ul></li>\n"
end
end
classes = (ancestors.empty? ? 'root' : 'child')
s << "<li class='#{classes}'><div class='#{classes}'>"
s << h(block_given? ? yield(project) : project.name)
s << "</div>\n"
ancestors << project
end
s << ("</li></ul>\n" * ancestors.size)
@project = original_project
end
s.html_safe
end
end
end

View File

@ -25,10 +25,12 @@ module RedmineCustomWorkflows
end end
def run_custom_workflows(on) def run_custom_workflows(on)
return true unless project && project.module_enabled?(:custom_workflows_module) return true unless project
workflows = project.enabled_custom_workflows
return true unless workflows.any?
@issue = self # compatibility with 0.0.1 @issue = self # compatibility with 0.0.1
Rails.logger.info "= Running #{on} custom workflows for issue \"#{subject}\" (##{id})" Rails.logger.info "= Running #{on} custom workflows for issue \"#{subject}\" (##{id})"
project.custom_workflows.each do |workflow| workflows.each do |workflow|
begin begin
Rails.logger.info "== Running #{on} custom workflow \"#{workflow.name}\"" Rails.logger.info "== Running #{on} custom workflow \"#{workflow.name}\""
instance_eval(workflow.read_attribute(on)) instance_eval(workflow.read_attribute(on))

View File

@ -3,6 +3,7 @@ module RedmineCustomWorkflows
unloadable unloadable
def self.included(base) def self.included(base)
base.send :include, InstanceMethods
base.class_eval do base.class_eval do
has_and_belongs_to_many :custom_workflows has_and_belongs_to_many :custom_workflows
#accepts_nested_attributes_for :custom_workflow, :update_only => true #accepts_nested_attributes_for :custom_workflow, :update_only => true
@ -10,5 +11,11 @@ module RedmineCustomWorkflows
lambda { |project, user| project.new_record? || user.allowed_to?(:manage_project_workflow, project) } lambda { |project, user| project.new_record? || user.allowed_to?(:manage_project_workflow, project) }
end end
end end
module InstanceMethods
def enabled_custom_workflows
(CustomWorkflow.for_all + custom_workflows).uniq.sort
end
end
end end
end end