New observable objects added (Project, Wiki Content, Attachment, Issue Attachments, Project Attachments, Wiki Page Attachments)

Ability to hook before_destroy and after_destroy events
This commit is contained in:
Anton Argirov 2015-11-22 16:44:16 +06:00
parent b9de089279
commit eab90b312f
16 changed files with 391 additions and 94 deletions

View File

@ -118,6 +118,8 @@ This plug-in is compatible with Redmine 2.x.x, 3.x.x
== Changelog == 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` [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. * 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 [0.1.3] Compatibility with Redmine 2.x.x returned, support of Redmine 1.x.x cancelled

View File

@ -8,9 +8,11 @@ class WorkflowError < StandardError
end end
class CustomWorkflow < ActiveRecord::Base class CustomWorkflow < ActiveRecord::Base
OBSERVABLES = [:issue, :user, :group, :group_users, :shared] OBSERVABLES = [:issue, :issue_attachments, :user, :attachment, :group, :group_users, :project, :project_attachments,
PROJECT_OBSERVABLES = [:issue] :wiki_content, :wiki_page_attachments, :shared]
COLLECTION_OBSERVABLES = [:group_users] 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 attr_protected :id
has_and_belongs_to_many :projects has_and_belongs_to_many :projects
@ -19,7 +21,7 @@ class CustomWorkflow < ActiveRecord::Base
validates_presence_of :name validates_presence_of :name
validates_uniqueness_of :name, :case_sensitive => false 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 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 if Rails::VERSION::MAJOR >= 4
default_scope { order(:position => :asc) } default_scope { order(:position => :asc) }
@ -53,7 +55,7 @@ class CustomWorkflow < ActiveRecord::Base
workflows = CustomWorkflow.observing(:shared).active workflows = CustomWorkflow.observing(:shared).active
log_message '= Running shared code', object log_message '= Running shared code', object
workflows.each do |workflow| workflows.each do |workflow|
if workflow.run(object, :shared_code) == false unless workflow.run(object, :shared_code)
log_message '= Abort running shared code', object log_message '= Abort running shared code', object
return false return false
end end
@ -64,14 +66,14 @@ class CustomWorkflow < ActiveRecord::Base
def run_custom_workflows(observable, object, event) def run_custom_workflows(observable, object, event)
workflows = CustomWorkflow.active.observing(observable) workflows = CustomWorkflow.active.observing(observable)
if object.respond_to? :project if PROJECT_OBSERVABLES.include? observable
return true unless object.project return true unless object.project
workflows = workflows.for_project(object.project) workflows = workflows.for_project(object.project)
end end
return true unless workflows.any? return true unless workflows.any?
log_message "= Running #{event} custom workflows", object log_message "= Running #{event} custom workflows", object
workflows.each do |workflow| workflows.each do |workflow|
if workflow.run(object, event) == false unless workflow.run(object, event)
log_message "= Abort running #{event} custom workflows", object log_message "= Abort running #{event} custom workflows", object
return false return false
end end
@ -82,6 +84,7 @@ class CustomWorkflow < ActiveRecord::Base
end end
def run(object, event) def run(object, event)
return true unless attribute_present?(event)
Rails.logger.info "== Running #{event} custom workflow \"#{name}\"" Rails.logger.info "== Running #{event} custom workflow \"#{name}\""
object.instance_eval(read_attribute(event)) object.instance_eval(read_attribute(event))
true true
@ -106,32 +109,60 @@ class CustomWorkflow < ActiveRecord::Base
errors.add event, :invalid_script, :error => e errors.add event, :invalid_script, :error => e
end 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 def validate_syntax
return unless respond_to?(:observable) && active? case observable.to_sym
case observable when :shared
when 'shared' CustomWorkflow.run_shared_code self
CustomWorkflow.run_shared_code(self) validate_syntax_for self, :shared_code
validate_syntax_for(self, :shared_code) when *SINGLE_OBSERVABLES
when 'user', 'group', 'issue'
object = observable.camelize.constantize.new object = observable.camelize.constantize.new
object.send :instance_variable_set, "@#{observable}", object # compatibility with 0.0.1 object.send :instance_variable_set, "@#{observable}", object # compatibility with 0.0.1
CustomWorkflow.run_shared_code(object) CustomWorkflow.run_shared_code object
validate_syntax_for(object, :before_save) [:before_save, :after_save, :before_destroy, :after_destroy].each {|field| validate_syntax_for object, field}
validate_syntax_for(object, :after_save) when *COLLECTION_OBSERVABLES
when 'group_users' object = nil
@user = User.new case observable.to_sym
@group = Group.new when :group_users
CustomWorkflow.run_shared_code(self) object = Group.new
validate_syntax_for(self, :before_add) object.send :instance_variable_set, :@user, User.new
validate_syntax_for(self, :before_remove) object.send :instance_variable_set, :@group, object
validate_syntax_for(self, :after_add) when :issue_attachments
validate_syntax_for(self, :after_remove) 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
end end
def export_as_xml def export_as_xml
only = [:author, :name, :description, :before_save, :after_save, :shared_code, :observable, 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] } only = only.select { |p| self[p] }
to_xml :only => only do |xml| to_xml :only => only do |xml|
xml.tag! 'exported-at', Time.current.xmlschema xml.tag! 'exported-at', Time.current.xmlschema

View File

@ -36,10 +36,18 @@
<fieldset class="box"> <fieldset class="box">
<legend><%= l(:label_workflow_scripts) %></legend> <legend><%= l(:label_workflow_scripts) %></legend>
<% case @workflow.observable %> <% observable = @workflow.observable.to_sym %>
<% when 'shared' %> <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' %> <%= f.text_area :shared_code, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
<% when 'group_users' %> <% 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="splitcontent">
<div class="splitcontentleft"> <div class="splitcontentleft">
<%= f.text_area :before_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %> <%= f.text_area :before_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
@ -48,7 +56,11 @@
<%= f.text_area :after_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %> <%= f.text_area :after_add, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div> </div>
</div> </div>
<div style="clear: left;"></div> <% 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="splitcontent">
<div class="splitcontentleft"> <div class="splitcontentleft">
<%= f.text_area :before_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %> <%= f.text_area :before_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
@ -57,33 +69,41 @@
<%= f.text_area :after_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %> <%= f.text_area :after_remove, :cols => 40, :rows => 20, :wrap => 'off', :class => 'custom_workflow_script' %>
</div> </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' %>
</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>
</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>
</div>
</div>
<div style="clear: left;"></div>
<% end %> <% end %>
<p> <% when *CustomWorkflow::SINGLE_OBSERVABLES %>
<em class="info"><%= l("text_custom_workflow_#{@workflow.observable}_code_note") %></em> <%= render :layout => 'layouts/collapsible',
</p> :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>
<% 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>
<% end %>
<% end %>
</fieldset> </fieldset>
<script type="text/javascript"> <script type="text/javascript">

View File

@ -38,7 +38,7 @@
<%= workflow.projects.map(&:name).join(", ") %> <%= workflow.projects.map(&:name).join(", ") %>
<% end %> <% end %>
</td> </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"> <td class="buttons">
<% if workflow.active? %> <% if workflow.active? %>
<%= link_to(l(:button_custom_workflow_deactivate), custom_workflow_status_path(workflow, :active => false), :class => 'icon icon-inactive', :method => :post) %> <%= link_to(l(:button_custom_workflow_deactivate), custom_workflow_status_path(workflow, :active => false), :class => 'icon icon-inactive', :method => :post) %>

View File

@ -0,0 +1,6 @@
<fieldset class="collapsible <%= collapsed ? 'collapsed' : '' %>">
<legend onclick="toggleFieldset(this);"><%= label %></legend>
<div style="<%= collapsed ? 'display: none' : '' %>">
<%= yield %>
</div>
</fieldset>

View File

@ -9,6 +9,10 @@ en:
label_enabled_projects: "Enabled for project(s)" label_enabled_projects: "Enabled for project(s)"
label_custom_workflow_export: "Export" label_custom_workflow_export: "Export"
label_custom_workflow_import: "Import workflow" 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_import: "Import"
button_custom_workflow_activate: "Activate" button_custom_workflow_activate: "Activate"
@ -16,6 +20,8 @@ en:
field_after_save: "Workflow script executable after saving observable object" field_after_save: "Workflow script executable after saving observable object"
field_before_save: "Workflow script executable before 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_after_add: "Workflow script executable after adding observable object to collection"
field_before_add: "Workflow script executable before 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" field_after_remove: "Workflow script executable after removing observable object from collection"
@ -40,15 +46,23 @@ en:
invalid_script: "contains error: %{error}" invalid_script: "contains error: %{error}"
custom_workflow_error: "Custom workflow error (please contact administrator)" custom_workflow_error: "Custom workflow error (please contact administrator)"
new_status_invalid: "transition from '%{old_status}' to '%{new_status}' is prohibited" 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_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_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_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_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_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: 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_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_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_no_enabled_projects: No projects
text_custom_workflow_author: Will be included in exported XML text_custom_workflow_author: Will be included in exported XML
text_custom_workflow_disabled: disabled by admin text_custom_workflow_disabled: disabled by admin
@ -56,6 +70,12 @@ en:
custom_workflow_observable_shared: "<shared code>" custom_workflow_observable_shared: "<shared code>"
custom_workflow_observable_issue: "Issue" custom_workflow_observable_issue: "Issue"
custom_workflow_observable_issue_attachments: "Issue Attachments"
custom_workflow_observable_group: "Group" custom_workflow_observable_group: "Group"
custom_workflow_observable_user: "User" custom_workflow_observable_user: "User"
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" custom_workflow_observable_group_users: "Group Users"

View File

@ -9,6 +9,10 @@ ru:
label_enabled_projects: "Разрешен в проектах" label_enabled_projects: "Разрешен в проектах"
label_custom_workflow_export: "Экспорт" label_custom_workflow_export: "Экспорт"
label_custom_workflow_import: "Импорт процесса" label_custom_workflow_import: "Импорт процесса"
label_save_workflows: "Сохранение наблюдаемых объектов"
label_destroy_workflows: "Уничтожение наблюдаемых объектов"
label_add_workflows: "Добавление наблюдаемых объектов в коллекцию"
label_remove_workflows: "Удаление наблюдаемых объектов из коллекции"
button_import: "Импортировать" button_import: "Импортировать"
button_custom_workflow_activate: "Активировать" button_custom_workflow_activate: "Активировать"
@ -16,6 +20,8 @@ ru:
field_after_save: "Сценарий выполняемый после сохранения наблюдаемого объекта" field_after_save: "Сценарий выполняемый после сохранения наблюдаемого объекта"
field_before_save: "Сценарий выполняемый перед сохранением наблюдаемого объекта" field_before_save: "Сценарий выполняемый перед сохранением наблюдаемого объекта"
field_after_destroy: "Сценарий выполняемый после уничтожения наблюдаемого объекта"
field_before_destroy: "Сценарий выполняемый перед уничтожением наблюдаемого объекта"
field_after_add: "Сценарий выполняемый после добавления наблюдаемого объекта в коллекцию" field_after_add: "Сценарий выполняемый после добавления наблюдаемого объекта в коллекцию"
field_before_add: "Сценарий выполняемый перед добавлением наблюдаемого объекта в коллекцию" field_before_add: "Сценарий выполняемый перед добавлением наблюдаемого объекта в коллекцию"
field_after_remove: "Сценарий выполняемый после удаления наблюдаемого объекта из коллекции" field_after_remove: "Сценарий выполняемый после удаления наблюдаемого объекта из коллекции"
@ -40,15 +46,23 @@ ru:
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}' невозможен"
scripts_absent: "Хотя бы один скрипт должен быть определен"
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_issue_code_note: Оба сценария исполняются в контексте задачи, как и обычные обратные вызовы before_save и after_save. Поэтому используйте методы и свойства задачи напрямую или через ключевое слово self. text_custom_workflow_issue_code_note: Эти сценарии исполняются в контексте задачи, как и обычные обратные вызовы before_save и after_save. Поэтому используйте методы и свойства задачи (Issue) напрямую или через ключевое слово self.
text_custom_workflow_shared_code_note: Этот код будет исполняться перед любым другим процессом и может содержать общий код, например, функции и классы, необходимые для работы других процессов. text_custom_workflow_shared_code_note: Этот код будет исполняться перед любым другим процессом и может содержать общий код, например, функции и классы, необходимые для работы других процессов.
text_custom_workflow_user_code_note: Оба сценария исполняются в контексте объекта пользователя когда объект пользователя изменяется. Используйте методы и свойства объекта пользователя напрямую или через ключевое слово self. text_custom_workflow_user_code_note: Эти сценарии исполняются в контексте объекта пользователя когда объект пользователя изменяется (удаляется). Используйте методы и свойства объекта пользователя (User) напрямую или через ключевое слово self.
text_custom_workflow_group_code_note: Оба сценария исполняются в контексте объекта группы когда объект группы изменяется. Используйте методы и свойства объекта группы напрямую или через ключевое слово self. text_custom_workflow_group_code_note: Эти сценарии исполняются в контексте объекта группы когда объект группы изменяется (удаляется). Используйте методы и свойства объекта группы (Group) напрямую или через ключевое слово self.
text_custom_workflow_group_users_code_note: Эти скрипты выполняются когда пользователь добавляется в группу/удаляется из группы. Используйте переменные @user и @group для доступа к соответствующим объектам из Ваших сценариев. text_custom_workflow_group_users_code_note: Эти сценарии выполняются когда пользователь добавляется в группу/удаляется из группы. Используйте переменные @user и @group для доступа к соответствующим объектам из Ваших сценариев.
text_custom_workflow_attachment_code_note: Эти сценарии исполняются в контексте объекта вложения когда объект вложения изменяется (удаляется). Используйте методы и свойства объекта вложения (Attachment) напрямую или через ключевое слово self. Обратите внимание на то, что данные сценарии выполняются при сохранении (удалении) вложения любого типа (задача, документ, страница Wiki и т.д.), поэтому следует дополнительно проверять в коде поле container_type либо в качестве наблюдаемого объекта выбрать конкретный тип вложения.
text_custom_workflow_issue_attachments_code_note: Эти сценарии выполняются когда вложение прикладывается к задаче/удаляется из задачи. Используйте переменные @issue и @attachment для доступа к соответствующим объектам из Ваших сценариев.
text_custom_workflow_project_code_note: Эти сценарии исполняются в контексте объекта проекта когда объект проекта изменяется (удаляется). Используйте методы и свойства объекта группы (Project) напрямую или через ключевое слово self.
text_custom_workflow_project_attachments_code_note: Эти сценарии выполняются когда файл загружается в проект/удаляется из проекта. Используйте переменные @project и @attachment для доступа к соответствующим объектам из Ваших сценариев.
text_custom_workflow_wiki_content_code_note: Эти сценарии исполняются в контексте объекта Wiki содержания когда объект Wiki содержания изменяется (удаляется). Используйте методы и свойства объекта содержания Wiki (WikiContent) напрямую или через ключевое слово self.
text_custom_workflow_wiki_page_attachments_code_note: Эти сценарии выполняются когда файл загружается на Wiki страницу/удаляется с Wiki страницы. Используйте переменные @page и @attachment для доступа к соответствующим объектам из Ваших сценариев.
text_no_enabled_projects: Нет проектов text_no_enabled_projects: Нет проектов
text_custom_workflow_author: Будет использован в XML файле при экспорте text_custom_workflow_author: Будет использован в XML файле при экспорте
text_custom_workflow_disabled: отключен администратором text_custom_workflow_disabled: отключен администратором
@ -56,6 +70,12 @@ ru:
custom_workflow_observable_shared: "<общий код>" custom_workflow_observable_shared: "<общий код>"
custom_workflow_observable_issue: "Задача" custom_workflow_observable_issue: "Задача"
custom_workflow_observable_issue_attachments: "Вложения задач"
custom_workflow_observable_group: "Группа" custom_workflow_observable_group: "Группа"
custom_workflow_observable_user: "Пользователь" custom_workflow_observable_user: "Пользователь"
custom_workflow_observable_attachment: "Вложение"
custom_workflow_observable_project: "Проект"
custom_workflow_observable_project_attachments: "Вложения проекта / Файлы"
custom_workflow_observable_wiki_content: "Содержание Wiki"
custom_workflow_observable_wiki_page_attachments: "Вложения страниц Wiki"
custom_workflow_observable_group_users: "Пользователи группы" custom_workflow_observable_group_users: "Пользователи группы"

View File

@ -0,0 +1,6 @@
class AddBeforeAndAfterDestroyToCustomWorkflows < ActiveRecord::Migration
def change
add_column :custom_workflows, :before_destroy, :text, :null => true
add_column :custom_workflows, :after_destroy, :text, :null => true
end
end

11
init.rb
View File

@ -8,6 +8,9 @@ Rails.application.config.to_prepare do
unless ProjectsHelper.include?(RedmineCustomWorkflows::ProjectsHelperPatch) unless ProjectsHelper.include?(RedmineCustomWorkflows::ProjectsHelperPatch)
ProjectsHelper.send(:include, RedmineCustomWorkflows::ProjectsHelperPatch) ProjectsHelper.send(:include, RedmineCustomWorkflows::ProjectsHelperPatch)
end end
unless Attachment.include?(RedmineCustomWorkflows::AttachmentPatch)
Attachment.send(:include, RedmineCustomWorkflows::AttachmentPatch)
end
unless Issue.include?(RedmineCustomWorkflows::IssuePatch) unless Issue.include?(RedmineCustomWorkflows::IssuePatch)
Issue.send(:include, RedmineCustomWorkflows::IssuePatch) Issue.send(:include, RedmineCustomWorkflows::IssuePatch)
end end
@ -17,6 +20,12 @@ Rails.application.config.to_prepare do
unless Group.include?(RedmineCustomWorkflows::GroupPatch) unless Group.include?(RedmineCustomWorkflows::GroupPatch)
Group.send(:include, RedmineCustomWorkflows::GroupPatch) Group.send(:include, RedmineCustomWorkflows::GroupPatch)
end end
unless WikiContent.include?(RedmineCustomWorkflows::WikiContentPatch)
WikiContent.send(:include, RedmineCustomWorkflows::WikiContentPatch)
end
unless WikiPage.include?(RedmineCustomWorkflows::WikiPagePatch)
WikiPage.send(:include, RedmineCustomWorkflows::WikiPagePatch)
end
unless Mailer.include?(RedmineCustomWorkflows::MailerPatch) unless Mailer.include?(RedmineCustomWorkflows::MailerPatch)
Mailer.send(:include, RedmineCustomWorkflows::MailerPatch) Mailer.send(:include, RedmineCustomWorkflows::MailerPatch)
end end
@ -29,7 +38,7 @@ 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.1.4' version '0.1.5'
url 'http://www.redmine.org/plugins/custom-workflows' url 'http://www.redmine.org/plugins/custom-workflows'
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

View File

@ -0,0 +1,38 @@
module RedmineCustomWorkflows
module AttachmentPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
before_save :before_save_custom_workflows
after_save :after_save_custom_workflows
before_destroy :before_destroy_custom_workflows
after_destroy :after_destroy_custom_workflows
end
end
module InstanceMethods
def before_save_custom_workflows
@attachment = self
@saved_attributes = attributes.dup
CustomWorkflow.run_shared_code(self)
CustomWorkflow.run_custom_workflows(:attachment, self, :before_save)
errors.empty? && (@saved_attributes == attributes || valid?)
ensure
@saved_attributes = nil
end
def after_save_custom_workflows
CustomWorkflow.run_custom_workflows(:attachment, self, :after_save)
end
def before_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:attachment, self, :before_destroy)
end
def after_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:attachment, self, :after_destroy)
end
end
end
end

View File

@ -6,6 +6,8 @@ module RedmineCustomWorkflows
base.class_eval do base.class_eval do
before_save :before_save_custom_workflows before_save :before_save_custom_workflows
after_save :after_save_custom_workflows after_save :after_save_custom_workflows
before_destroy :before_destroy_custom_workflows
after_destroy :after_destroy_custom_workflows
def self.users_callback(event, group, user) def self.users_callback(event, group, user)
group.instance_variable_set(:@group, group) group.instance_variable_set(:@group, group)
@ -13,17 +15,12 @@ module RedmineCustomWorkflows
CustomWorkflow.run_shared_code(group) if event.to_s.starts_with? 'before_' CustomWorkflow.run_shared_code(group) if event.to_s.starts_with? 'before_'
CustomWorkflow.run_custom_workflows(:group_users, group, event) CustomWorkflow.run_custom_workflows(:group_users, group, event)
end end
if Rails::VERSION::MAJOR >= 4 [:before_add, :before_remove, :after_add, :after_remove].each do |observable|
callback_lambda = lambda { |event, group, user| Group.users_callback(event, group, user) } send("#{observable}_for_users") << if Rails::VERSION::MAJOR >= 4
before_add_for_users << callback_lambda lambda { |event, group, user| Group.users_callback(event, group, user) }
before_remove_for_users << callback_lambda
after_add_for_users << callback_lambda
after_remove_for_users << callback_lambda
else else
before_add_for_users << lambda { |group, user| Group.users_callback(:before_add, group, user) } lambda { |group, user| Group.users_callback(observable, group, user) }
before_remove_for_users << lambda { |group, user| Group.users_callback(:before_remove, group, user) } end
after_add_for_users << lambda { |group, user| Group.users_callback(:after_add, group, user) }
after_remove_for_users << lambda { |group, user| Group.users_callback(:after_remove, group, user) }
end end
end end
end end
@ -42,6 +39,14 @@ module RedmineCustomWorkflows
def after_save_custom_workflows def after_save_custom_workflows
CustomWorkflow.run_custom_workflows(:group, self, :after_save) CustomWorkflow.run_custom_workflows(:group, self, :after_save)
end end
def before_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:group, self, :before_destroy)
end
def after_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:group, self, :after_destroy)
end
end end
end end
end end

View File

@ -6,7 +6,24 @@ module RedmineCustomWorkflows
base.class_eval do base.class_eval do
before_save :before_save_custom_workflows before_save :before_save_custom_workflows
after_save :after_save_custom_workflows after_save :after_save_custom_workflows
before_destroy :before_destroy_custom_workflows
after_destroy :after_destroy_custom_workflows
validate :validate_status validate :validate_status
def self.attachments_callback(event, issue, attachment)
issue.instance_variable_set(:@issue, issue)
issue.instance_variable_set(:@attachment, attachment)
CustomWorkflow.run_shared_code(issue) if event.to_s.starts_with? 'before_'
CustomWorkflow.run_custom_workflows(:issue_attachments, issue, event)
end
[:before_add, :before_remove, :after_add, :after_remove].each do |observable|
send("#{observable}_for_attachments") << if Rails::VERSION::MAJOR >= 4
lambda { |event, issue, attachment| Issue.attachments_callback(event, issue, attachment) }
else
lambda { |issue, attachment| Issue.attachments_callback(observable, issue, attachment) }
end
end
end end
end end
@ -36,6 +53,14 @@ module RedmineCustomWorkflows
def after_save_custom_workflows def after_save_custom_workflows
CustomWorkflow.run_custom_workflows(:issue, self, :after_save) CustomWorkflow.run_custom_workflows(:issue, self, :after_save)
end end
def before_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:issue, self, :before_destroy)
end
def after_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:issue, self, :after_destroy)
end
end end
end end
end end

View File

@ -5,13 +5,53 @@ module RedmineCustomWorkflows
base.send :include, InstanceMethods 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
safe_attributes :custom_workflow_ids, :if => safe_attributes :custom_workflow_ids, :if =>
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) }
before_save :before_save_custom_workflows
after_save :after_save_custom_workflows
before_destroy :before_destroy_custom_workflows
after_destroy :after_destroy_custom_workflows
def self.attachments_callback(event, project, attachment)
project.instance_variable_set(:@project, project)
project.instance_variable_set(:@attachment, attachment)
CustomWorkflow.run_shared_code(project) if event.to_s.starts_with? 'before_'
CustomWorkflow.run_custom_workflows(:project_attachments, project, event)
end
[:before_add, :before_remove, :after_add, :after_remove].each do |observable|
send("#{observable}_for_attachments") << if Rails::VERSION::MAJOR >= 4
lambda { |event, project, attachment| Project.attachments_callback(event, project, attachment) }
else
lambda { |project, attachment| Project.attachments_callback(observable, project, attachment) }
end
end
end end
end end
module InstanceMethods module InstanceMethods
def before_save_custom_workflows
@project = self
@saved_attributes = attributes.dup
CustomWorkflow.run_shared_code(self)
CustomWorkflow.run_custom_workflows(:project, self, :before_save)
errors.empty? && (@saved_attributes == attributes || valid?)
ensure
@saved_attributes = nil
end
def after_save_custom_workflows
CustomWorkflow.run_custom_workflows(:project, self, :after_save)
end
def before_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:project, self, :before_destroy)
end
def after_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:project, self, :after_destroy)
end
end end
end end
end end

View File

@ -6,6 +6,8 @@ module RedmineCustomWorkflows
base.class_eval do base.class_eval do
before_save :before_save_custom_workflows before_save :before_save_custom_workflows
after_save :after_save_custom_workflows after_save :after_save_custom_workflows
before_destroy :before_destroy_custom_workflows
after_destroy :after_destroy_custom_workflows
end end
end end
@ -23,6 +25,14 @@ module RedmineCustomWorkflows
def after_save_custom_workflows def after_save_custom_workflows
CustomWorkflow.run_custom_workflows(:user, self, :after_save) CustomWorkflow.run_custom_workflows(:user, self, :after_save)
end end
def before_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:user, self, :before_destroy)
end
def after_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:user, self, :after_destroy)
end
end end
end end
end end

View File

@ -0,0 +1,38 @@
module RedmineCustomWorkflows
module WikiContentPatch
def self.included(base)
base.send(:include, InstanceMethods)
base.class_eval do
before_save :before_save_custom_workflows
after_save :after_save_custom_workflows
before_destroy :before_destroy_custom_workflows
after_destroy :after_destroy_custom_workflows
end
end
module InstanceMethods
def before_save_custom_workflows
@content = self
@saved_attributes = attributes.dup
CustomWorkflow.run_shared_code(self)
CustomWorkflow.run_custom_workflows(:wiki_content, self, :before_save)
errors.empty? && (@saved_attributes == attributes || valid?)
ensure
@saved_attributes = nil
end
def after_save_custom_workflows
CustomWorkflow.run_custom_workflows(:wiki_content, self, :after_save)
end
def before_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:wiki_content, self, :before_destroy)
end
def after_destroy_custom_workflows
CustomWorkflow.run_custom_workflows(:wiki_content, self, :after_destroy)
end
end
end
end

View File

@ -0,0 +1,27 @@
module RedmineCustomWorkflows
module WikiPagePatch
def self.included(base)
base.send :include, InstanceMethods
base.class_eval do
def self.attachments_callback(event, page, attachment)
page.instance_variable_set(:@page, page)
page.instance_variable_set(:@attachment, attachment)
CustomWorkflow.run_shared_code(page) if event.to_s.starts_with? 'before_'
CustomWorkflow.run_custom_workflows(:wiki_page_attachments, page, event)
end
[:before_add, :before_remove, :after_add, :after_remove].each do |observable|
send("#{observable}_for_attachments") << if Rails::VERSION::MAJOR >= 4
lambda { |event, page, attachment| WikiPage.attachments_callback(event, page, attachment) }
else
lambda { |page, attachment| WikiPage.attachments_callback(observable, page, attachment) }
end
end
end
end
module InstanceMethods
end
end
end