Skip to content

Commit

Permalink
New observable objects added (Project, Wiki Content, Attachment, Issu…
Browse files Browse the repository at this point in the history
…e Attachments, Project Attachments, Wiki Page Attachments)

Ability to hook before_destroy and after_destroy events
  • Loading branch information
anteo committed Nov 22, 2015
1 parent b9de089 commit eab90b3
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 90 deletions.
2 changes: 2 additions & 0 deletions README.rdoc
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ This plug-in is compatible with Redmine 2.x.x, 3.x.x

== Changelog

[0.1.5] * New observable objects added (Project, Wiki Content, Attachment, Issue Attachments, Project Attachments, Wiki Page Attachments)
* Ability to hook before_destroy and after_destroy events
[0.1.4] * Ability to exit current workflow with `return` or `return true` and cancel workflow's execution chain with `return false`
* Non-active workflows are now not checked for syntax. Now you can import non-valid (for your Redmine instance for example) workflow, make changes to it and then activate.
[0.1.3] Compatibility with Redmine 2.x.x returned, support of Redmine 1.x.x cancelled
Expand Down
81 changes: 56 additions & 25 deletions app/models/custom_workflow.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ def initialize(message)
end

class CustomWorkflow < ActiveRecord::Base
OBSERVABLES = [:issue, :user, :group, :group_users, :shared]
PROJECT_OBSERVABLES = [:issue]
COLLECTION_OBSERVABLES = [:group_users]
OBSERVABLES = [:issue, :issue_attachments, :user, :attachment, :group, :group_users, :project, :project_attachments,
:wiki_content, :wiki_page_attachments, :shared]
PROJECT_OBSERVABLES = [:issue, :issue_attachments, :project, :project_attachments, :wiki_content, :wiki_page_attachments]
COLLECTION_OBSERVABLES = [:group_users, :issue_attachments, :project_attachments, :wiki_page_attachments]
SINGLE_OBSERVABLES = [:issue, :user, :group, :attachment, :project, :wiki_content]

attr_protected :id
has_and_belongs_to_many :projects
Expand All @@ -19,7 +21,7 @@ class CustomWorkflow < ActiveRecord::Base
validates_presence_of :name
validates_uniqueness_of :name, :case_sensitive => false
validates_format_of :author, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
validate :validate_syntax
validate :validate_syntax, :validate_scripts_presence, :if => Proc.new {|workflow| workflow.respond_to?(:observable) and workflow.active?}

if Rails::VERSION::MAJOR >= 4
default_scope { order(:position => :asc) }
Expand Down Expand Up @@ -53,7 +55,7 @@ def run_shared_code(object)
workflows = CustomWorkflow.observing(:shared).active
log_message '= Running shared code', object
workflows.each do |workflow|
if workflow.run(object, :shared_code) == false
unless workflow.run(object, :shared_code)
log_message '= Abort running shared code', object
return false
end
Expand All @@ -64,14 +66,14 @@ def run_shared_code(object)

def run_custom_workflows(observable, object, event)
workflows = CustomWorkflow.active.observing(observable)
if object.respond_to? :project
if PROJECT_OBSERVABLES.include? observable
return true unless object.project
workflows = workflows.for_project(object.project)
end
return true unless workflows.any?
log_message "= Running #{event} custom workflows", object
workflows.each do |workflow|
if workflow.run(object, event) == false
unless workflow.run(object, event)
log_message "= Abort running #{event} custom workflows", object
return false
end
Expand All @@ -82,6 +84,7 @@ def run_custom_workflows(observable, object, event)
end

def run(object, event)
return true unless attribute_present?(event)
Rails.logger.info "== Running #{event} custom workflow \"#{name}\""
object.instance_eval(read_attribute(event))
true
Expand All @@ -106,32 +109,60 @@ def validate_syntax_for(object, event)
errors.add event, :invalid_script, :error => e
end

def validate_scripts_presence
case observable.to_sym
when :shared
fields = [shared_code]
when *SINGLE_OBSERVABLES
fields = [before_save, after_save, before_destroy, after_destroy]
when *COLLECTION_OBSERVABLES
fields = [before_add, after_add, before_remove, after_remove]
else
fields = []
end
unless fields.any? {|field| field.present?}
errors.add :base, :scripts_absent
end
end

def validate_syntax
return unless respond_to?(:observable) && active?
case observable
when 'shared'
CustomWorkflow.run_shared_code(self)
validate_syntax_for(self, :shared_code)
when 'user', 'group', 'issue'
case observable.to_sym
when :shared
CustomWorkflow.run_shared_code self
validate_syntax_for self, :shared_code
when *SINGLE_OBSERVABLES
object = observable.camelize.constantize.new
object.send :instance_variable_set, "@#{observable}", object # compatibility with 0.0.1
CustomWorkflow.run_shared_code(object)
validate_syntax_for(object, :before_save)
validate_syntax_for(object, :after_save)
when 'group_users'
@user = User.new
@group = Group.new
CustomWorkflow.run_shared_code(self)
validate_syntax_for(self, :before_add)
validate_syntax_for(self, :before_remove)
validate_syntax_for(self, :after_add)
validate_syntax_for(self, :after_remove)
CustomWorkflow.run_shared_code object
[:before_save, :after_save, :before_destroy, :after_destroy].each {|field| validate_syntax_for object, field}
when *COLLECTION_OBSERVABLES
object = nil
case observable.to_sym
when :group_users
object = Group.new
object.send :instance_variable_set, :@user, User.new
object.send :instance_variable_set, :@group, object
when :issue_attachments
object = Issue.new
object.send :instance_variable_set, :@attachment, Attachment.new
object.send :instance_variable_set, :@issue, object
when :project_attachments
object = Project.new
object.send :instance_variable_set, :@attachment, Attachment.new
object.send :instance_variable_set, :@project, object
when :wiki_page_attachments
object = WikiPage.new
object.send :instance_variable_set, :@attachment, Attachment.new
object.send :instance_variable_set, :@page, object
end
CustomWorkflow.run_shared_code self
[:before_add, :after_add, :before_remove, :after_remove].each {|field| validate_syntax_for object, field}
end
end

def export_as_xml
only = [:author, :name, :description, :before_save, :after_save, :shared_code, :observable,
:before_add, :after_add, :before_remove, :after_remove, :created_at]
:before_add, :after_add, :before_remove, :after_remove, :before_destroy, :after_destroy, :created_at]
only = only.select { |p| self[p] }
to_xml :only => only do |xml|
xml.tag! 'exported-at', Time.current.xmlschema
Expand Down
104 changes: 62 additions & 42 deletions app/views/custom_workflows/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -36,54 +36,74 @@

<fieldset class="box">
<legend><%= l(:label_workflow_scripts) %></legend>
<% case @workflow.observable %>
<% when 'shared' %>
<% observable = @workflow.observable.to_sym %>
<p>
<em class="info"><%= l("text_custom_workflow_#{observable}_code_note") %></em>
</p>
<% case observable %>
<% when :shared %>
<%= f.text_area :shared_code, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<% when 'group_users' %>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
<div class="splitcontentright">
<%= f.text_area :after_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
</div>
<div style="clear: left;"></div>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
<div class="splitcontentright">
<%= f.text_area :after_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<% when *CustomWorkflow::COLLECTION_OBSERVABLES %>
<%= render :layout => 'layouts/collapsible',
:locals => {
:collapsed => (not (@workflow.before_add.present? or @workflow.after_add.present? or @workflow.errors[:base].present?)),
:label => l(:label_add_workflows)} do %>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
<div class="splitcontentright">
<%= f.text_area :after_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
</div>
</div>
<div style="clear: left;"></div>
<% when 'user', 'group' %>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
<div class="splitcontentright">
<%= f.text_area :after_save, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<% end %>
<%= render :layout => 'layouts/collapsible',
:locals => {
:collapsed => (not (@workflow.before_remove.present? or @workflow.after_remove.present?)),
:label => l(:label_remove_workflows)} do %>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
<div class="splitcontentright">
<%= f.text_area :after_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
</div>
</div>
<div style="clear: left;"></div>
<% when 'issue' %>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<em class="info"><%= l(:text_custom_workflow_before_save_note) %></em>
<% end %>
<% when *CustomWorkflow::SINGLE_OBSERVABLES %>
<%= render :layout => 'layouts/collapsible',
:locals => {
:collapsed => (not (@workflow.before_save.present? or @workflow.after_save.present? or @workflow.errors[:base].present?)),
:label => l(:label_save_workflows)} do %>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_save, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<% if observable == :issue %>
<em class="info"><%= l(:text_custom_workflow_before_save_note) %></em>
<% end %>
</div>
<div class="splitcontentright">
<%= f.text_area :after_save, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<% if observable == :issue %>
<em class="info"><%= l(:text_custom_workflow_after_save_note) %></em>
<% end %>
</div>
</div>
<div class="splitcontentright">
<%= f.text_area :after_save, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<em class="info"><%= l(:text_custom_workflow_after_save_note) %></em>
<% end %>
<%= render :layout => 'layouts/collapsible',
:locals => {
:collapsed => (not (@workflow.before_destroy.present? or @workflow.before_destroy.present?)),
:label => l(:label_destroy_workflows)} do %>
<div class="splitcontent">
<div class="splitcontentleft">
<%= f.text_area :before_destroy, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
<div class="splitcontentright">
<%= f.text_area :after_destroy, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div>
</div>
</div>
<div style="clear: left;"></div>
<% end %>
<% end %>
<p>
<em class="info"><%= l("text_custom_workflow_#{@workflow.observable}_code_note") %></em>
</p>
</fieldset>

<script type="text/javascript">
Expand Down
2 changes: 1 addition & 1 deletion app/views/custom_workflows/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
<%= workflow.projects.map(&:name).join(", ") %>
<% end %>
</td>
<td align="center"><%= reorder_links("custom_workflow", {:action => 'update', :id => workflow, :commit => true}) %></td>
<td align="center" nowrap><%= reorder_links("custom_workflow", {:action => 'update', :id => workflow, :commit => true}) %></td>
<td class="buttons">
<% if workflow.active? %>
<%= link_to(l(:button_custom_workflow_deactivate), custom_workflow_status_path(workflow, :active => false), :class => 'icon icon-inactive', :method => :post) %>
Expand Down
6 changes: 6 additions & 0 deletions app/views/layouts/_collapsible.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<fieldset class="collapsible <%= collapsed ? 'collapsed' : '' %>">
<legend onclick="toggleFieldset(this);"><%= label %></legend>
<div style="<%= collapsed ? 'display: none' : '' %>">
<%= yield %>
</div>
</fieldset>
28 changes: 24 additions & 4 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@ en:
label_enabled_projects: "Enabled for project(s)"
label_custom_workflow_export: "Export"
label_custom_workflow_import: "Import workflow"
label_save_workflows: "Saving observable objects"
label_destroy_workflows: "Destroying observable objects"
label_add_workflows: "Adding observable objects to collection"
label_remove_workflows: "Removing observable objects from collection"

button_import: "Import"
button_custom_workflow_activate: "Activate"
button_custom_workflow_deactivate: "Deactivate"

field_after_save: "Workflow script executable after saving observable object"
field_before_save: "Workflow script executable before saving observable object"
field_after_destroy: "Workflow script executable after destroying observable object"
field_before_destroy: "Workflow script executable before destroying observable object"
field_after_add: "Workflow script executable after adding observable object to collection"
field_before_add: "Workflow script executable before adding observable object to collection"
field_after_remove: "Workflow script executable after removing observable object from collection"
Expand All @@ -40,22 +46,36 @@ en:
invalid_script: "contains error: %{error}"
custom_workflow_error: "Custom workflow error (please contact administrator)"
new_status_invalid: "transition from '%{old_status}' to '%{new_status}' is prohibited"
scripts_absent: "At least one script should be defined"

text_select_project_custom_workflows: Select project custom workflows
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_issue_code_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_issue_code_note: Scripts are executed in the context of Issue object 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_shared_code_note: This code will run before any other workflow and may contain shared code, e.g. functions and classes needed by other workflows
text_custom_workflow_user_code_note: Both scripts are executed in the context of the user object when user object changes. Use methods and properties of the user directly (or through "self")
text_custom_workflow_group_code_note: Both scripts are executed in the context of the group object when group object changes. Use methods and properties of the group directly (or through "self")
text_custom_workflow_user_code_note: Scripts are executed in the context of User object when user object changes (destroys). Use methods and properties of the user directly (or through "self")
text_custom_workflow_group_code_note: Scripts are executed in the context of Group object when group object changes (destroys). Use methods and properties of the group directly (or through "self")
text_custom_workflow_group_users_code_note: These scripts are executed when user being added to group/removed from group. Use variables @user and @group to access appropriate objects in your scripts.
text_custom_workflow_attachment_code_note: Scripts are executed in the context of Attachment object when attachment object changes (destroys). Use methods and properties of the attachment object directly (or through "self"). Note that these scripts will affect all attachment types (issue, document, wiki pages and etc), so you should check 'container_type' field additionally in your script or select specific "... Attachments" observable object.
text_custom_workflow_issue_attachments_code_note: These scripts are executed when attachment being added to issue/removed from issue. Use variables @issue and @attachment to access appropriate objects in your scripts.
text_custom_workflow_project_code_note: Scripts are executed in the context of Project object when project object changes (destroys). Use methods and properties of the project directly (or through "self")
text_custom_workflow_project_attachments_code_note: These scripts are executed when a file being added to project/removed from project. Use variables @project and @attachment to access appropriate objects in your scripts.
text_custom_workflow_wiki_content_code_note: Scripts are executed in the context of Wiki Content object when project object changes (destroys). Use methods and properties of the project directly (or through "self")
text_custom_workflow_wiki_page_attachments_code_note: These scripts are executed when a file being added to wiki page/removed from wiki page. Use variables @page and @attachment to access appropriate objects in your scripts.

text_no_enabled_projects: No projects
text_custom_workflow_author: Will be included in exported XML
text_custom_workflow_disabled: disabled by admin
text_custom_workflow_is_for_all: enabled for all projects

custom_workflow_observable_shared: "<shared code>"
custom_workflow_observable_issue: "Issue"
custom_workflow_observable_issue_attachments: "Issue Attachments"
custom_workflow_observable_group: "Group"
custom_workflow_observable_user: "User"
custom_workflow_observable_group_users: "Group Users"
custom_workflow_observable_attachment: "Attachment"
custom_workflow_observable_project: "Project"
custom_workflow_observable_project_attachments: "Project Attachments / Files"
custom_workflow_observable_wiki_content: "Wiki Content"
custom_workflow_observable_wiki_page_attachments: "Wiki Page Attachments"
custom_workflow_observable_group_users: "Group Users"
Loading

0 comments on commit eab90b3

Please sign in to comment.