From f2fbce3b10c5c47d80e3e9235b43e72c57a8508a Mon Sep 17 00:00:00 2001 From: Anton Argirov Date: Mon, 8 Oct 2012 14:19:46 +0700 Subject: [PATCH] Allow custom workflows to be enabled globally for all projects (#8750) --- README.rdoc | 6 ++- .../custom_workflows_controller.rb | 2 +- app/models/custom_workflow.rb | 12 +++++ app/views/custom_workflows/_form.html.erb | 53 ++++++++++++++----- app/views/custom_workflows/index.html.erb | 10 ++++ .../settings/_custom_workflow.html.erb | 2 +- assets/stylesheets/style.css | 4 +- config/locales/en.yml | 13 +++-- config/locales/ru.yml | 13 +++-- ...5252_add_is_for_all_to_custom_workflows.rb | 8 +++ init.rb | 16 +++--- lib/redmine_custom_workflows/helper.rb | 38 +++++++++++++ lib/redmine_custom_workflows/issue_patch.rb | 6 ++- lib/redmine_custom_workflows/project_patch.rb | 7 +++ 14 files changed, 154 insertions(+), 36 deletions(-) create mode 100644 db/migrate/20121005085252_add_is_for_all_to_custom_workflows.rb create mode 100644 lib/redmine_custom_workflows/helper.rb diff --git a/README.rdoc b/README.rdoc index 5876f0e..4eb229e 100644 --- a/README.rdoc +++ b/README.rdoc @@ -35,7 +35,9 @@ Both scripts are executed in the context of the issue. So access properties and == Enabling workflows for projects -After you defined your custom workflow(s), you need to enable it. Open Project settings. Go to the Enabled modules. Enable the Custom workflows module there and click *Save*. Then go to the Custom workflows 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 Project settings. Go to the Custom workflows tab of the project settings and enable workflow(s) you need for this project. 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 +[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.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 diff --git a/app/controllers/custom_workflows_controller.rb b/app/controllers/custom_workflows_controller.rb index 431151d..a9cddf5 100644 --- a/app/controllers/custom_workflows_controller.rb +++ b/app/controllers/custom_workflows_controller.rb @@ -6,7 +6,7 @@ class CustomWorkflowsController < ApplicationController before_filter :find_workflow, :only => [:show, :edit, :update, :destroy] def index - @workflows = CustomWorkflow.all + @workflows = CustomWorkflow.find(:all, :include => [:projects]) respond_to do |format| format.html end diff --git a/app/models/custom_workflow.rb b/app/models/custom_workflow.rb index 793ec8a..84fc995 100644 --- a/app/models/custom_workflow.rb +++ b/app/models/custom_workflow.rb @@ -7,6 +7,8 @@ class WorkflowError < StandardError end class CustomWorkflow < ActiveRecord::Base + unloadable + has_and_belongs_to_many :projects acts_as_list @@ -15,6 +17,12 @@ class CustomWorkflow < ActiveRecord::Base validates_uniqueness_of :name, :case_sensitive => false 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 issue = Issue.new issue.send :instance_variable_set, :@issue, issue # compatibility with 0.0.1 @@ -32,6 +40,10 @@ class CustomWorkflow < ActiveRecord::Base end end + def <=>(other) + self.position <=> other.position + end + def to_s name end diff --git a/app/views/custom_workflows/_form.html.erb b/app/views/custom_workflows/_form.html.erb index b0c85a2..d86aa9e 100644 --- a/app/views/custom_workflows/_form.html.erb +++ b/app/views/custom_workflows/_form.html.erb @@ -1,24 +1,53 @@ -
-

<%= f.text_field :name, :required => true, :size => 50 %>

-

<%= f.text_area :description, :cols => 80, :rows => 5 %>

-

- - <%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off', :no_label => true %> +

+
+

<%= f.text_field :name, :required => true, :size => 50 %>

+

<%= f.text_area :description, :cols => 40, :rows => 5 %>

+

+
+ +
+
+ <%= l(:label_enabled_projects) %> + <%= 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) %> +

<%= check_all_links 'custom_workflow_enabled_projects' %>

+
+
+
+ +
+ +
+ <%= l(:label_workflow_scripts) %> +
+
+ <%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off' %> <%= l(:text_custom_workflow_before_save_note) %> - - - <%= 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' %> <%= l(:text_custom_workflow_after_save_note) %> - -

+
+
+

<%= l(:text_custom_workflow_general_note) %>

-
+ <%= wikitoolbar_for :custom_workflow_description %> diff --git a/app/views/custom_workflows/index.html.erb b/app/views/custom_workflows/index.html.erb index 3038c23..e3399e7 100644 --- a/app/views/custom_workflows/index.html.erb +++ b/app/views/custom_workflows/index.html.erb @@ -12,6 +12,7 @@ <%=l(:field_name)%> <%=l(:field_description)%> + <%=l(:label_project_plural)%> <%= l(:button_sort) %> @@ -21,6 +22,15 @@ "> <%= link_to(workflow.name, edit_custom_workflow_path(workflow)) %> <%= textilizable(workflow.description) %> + + <% 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 %> + <%= reorder_links("custom_workflow", {:action => 'update', :id => workflow}) %> <%= 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) %> diff --git a/app/views/projects/settings/_custom_workflow.html.erb b/app/views/projects/settings/_custom_workflow.html.erb index c1b099f..1f4ee96 100644 --- a/app/views/projects/settings/_custom_workflow.html.erb +++ b/app/views/projects/settings/_custom_workflow.html.erb @@ -6,7 +6,7 @@ <% if CustomWorkflow.exists? %>
<% CustomWorkflow.all.each do |w| %> -
+
<%= textilizable(w.description) %>
<% end %>
diff --git a/assets/stylesheets/style.css b/assets/stylesheets/style.css index e911648..221973e 100644 --- a/assets/stylesheets/style.css +++ b/assets/stylesheets/style.css @@ -4,6 +4,8 @@ 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_enabled_projects ul { max-height: 200px; overflow-y: auto; } diff --git a/config/locales/en.yml b/config/locales/en.yml index 99c8c4c..94a0ba7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4,11 +4,13 @@ en: label_custom_workflow: "Custom workflows" label_custom_workflow_plural: "Custom workflows" label_custom_workflow_new: "Create a custom workflow" - label_workflow_scripts: "Workflow scripts" - field_after_save: "Workflow script (after save)" - field_before_save: "Workflow script (before save)" + label_enabled_projects: "Enabled for project(s)" + + 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_enabled_for_all_projects: "Enabled for all projects" field_custom_workflow: script: "Workflow script" @@ -21,6 +23,7 @@ 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_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_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: 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_no_enabled_projects: No projects \ No newline at end of file diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 791bf72..0827c0d 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -4,11 +4,13 @@ ru: label_custom_workflow: "Пользовательский рабочий процесс" label_custom_workflow_plural: "Пользовательские рабочие процессы" label_custom_workflow_new: "Новый процесс" - label_workflow_scripts: "Сценарии" - field_after_save: "Сценарий (после сохранения задачи)" - field_before_save: "Сценарий (перед сохранением задачи)" + label_enabled_projects: "Разрешен в проектах" + + field_after_save: "Сценарий выполняемый после сохранения задачи" + field_before_save: "Сценарий выполняемый перед сохранением задачи" field_is_enabled: "Разрешено" + field_enabled_for_all_projects: "Разрешен для всех проектов" field_custom_workflow: script: "Сценарий" @@ -21,6 +23,7 @@ ru: new_status_invalid: "- переход от '%{old_status}' к '%{new_status}' невозможен" text_select_project_custom_workflows: Выберите процессы для данного проекта - text_custom_workflow_before_save_note: Этот сценарий будет выполнен ПЕРЕД сохранением задачи. Здесь вы можете изменять свойства задачи. Не создавайте и не обновляйте связанные задачи в этом сценарии. Чтобы завершить сценарий с произвольной ошибкой, используйте raise WorkflowError, "Message to user". - text_custom_workflow_after_save_note: Этот сценарий будет выполнен ПОСЛЕ сохранения задачи. Вы можете обновлять и создавать задачи (в том числе и связанные задачи) здесь. Обратите внимание, что данный сценарий будет также выполняться и для вновь создаваемых задач. Поэтому используйте дополнительные проверки, чтобы избежать бесконечной рекурсии. + 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. + text_no_enabled_projects: Нет проектов diff --git a/db/migrate/20121005085252_add_is_for_all_to_custom_workflows.rb b/db/migrate/20121005085252_add_is_for_all_to_custom_workflows.rb new file mode 100644 index 0000000..716315f --- /dev/null +++ b/db/migrate/20121005085252_add_is_for_all_to_custom_workflows.rb @@ -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 diff --git a/init.rb b/init.rb index 8622006..5bb74ef 100644 --- a/init.rb +++ b/init.rb @@ -2,15 +2,18 @@ require 'redmine' require 'redmine_custom_workflows/hooks' to_prepare = Proc.new do - unless Project.included_modules.include?(RedmineCustomWorkflows::ProjectPatch) + unless Project.include?(RedmineCustomWorkflows::ProjectPatch) Project.send(:include, RedmineCustomWorkflows::ProjectPatch) end - unless ProjectsHelper.included_modules.include?(RedmineCustomWorkflows::ProjectsHelperPatch) + unless ProjectsHelper.include?(RedmineCustomWorkflows::ProjectsHelperPatch) ProjectsHelper.send(:include, RedmineCustomWorkflows::ProjectsHelperPatch) end - unless Issue.included_modules.include?(RedmineCustomWorkflows::IssuePatch) + unless Issue.include?(RedmineCustomWorkflows::IssuePatch) Issue.send(:include, RedmineCustomWorkflows::IssuePatch) end + unless ActionView::Base.include?(RedmineCustomWorkflows::Helper) + ActionView::Base.send(:include, RedmineCustomWorkflows::Helper) + end end if Redmine::VERSION::MAJOR >= 2 @@ -24,13 +27,10 @@ Redmine::Plugin.register :redmine_custom_workflows do name 'Redmine Custom Workflow plugin' author 'Anton Argirov' 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' 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 - end - + permission :manage_project_workflow, {}, :require => :member end diff --git a/lib/redmine_custom_workflows/helper.rb b/lib/redmine_custom_workflows/helper.rb new file mode 100644 index 0000000..69c3e95 --- /dev/null +++ b/lib/redmine_custom_workflows/helper.rb @@ -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 << "\n" + end + end + classes = (ancestors.empty? ? 'root' : 'child') + s << "
  • " + s << h(block_given? ? yield(project) : project.name) + s << "
    \n" + ancestors << project + end + s << ("
  • \n" * ancestors.size) + @project = original_project + end + s.html_safe + end + end +end \ No newline at end of file diff --git a/lib/redmine_custom_workflows/issue_patch.rb b/lib/redmine_custom_workflows/issue_patch.rb index fe35123..2d19494 100644 --- a/lib/redmine_custom_workflows/issue_patch.rb +++ b/lib/redmine_custom_workflows/issue_patch.rb @@ -25,10 +25,12 @@ module RedmineCustomWorkflows end 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 Rails.logger.info "= Running #{on} custom workflows for issue \"#{subject}\" (##{id})" - project.custom_workflows.each do |workflow| + workflows.each do |workflow| begin Rails.logger.info "== Running #{on} custom workflow \"#{workflow.name}\"" instance_eval(workflow.read_attribute(on)) diff --git a/lib/redmine_custom_workflows/project_patch.rb b/lib/redmine_custom_workflows/project_patch.rb index 079a119..44331eb 100644 --- a/lib/redmine_custom_workflows/project_patch.rb +++ b/lib/redmine_custom_workflows/project_patch.rb @@ -3,6 +3,7 @@ module RedmineCustomWorkflows unloadable def self.included(base) + base.send :include, InstanceMethods base.class_eval do has_and_belongs_to_many :custom_workflows #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) } end end + + module InstanceMethods + def enabled_custom_workflows + (CustomWorkflow.for_all + custom_workflows).uniq.sort + end + end end end