redmine_avm 0.3.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/app/controllers/issue_status_assigns_controller.rb +23 -0
  3. data/app/helpers/issue_status_assigns_helper.rb +11 -0
  4. data/app/models/issue_status_assign.rb +9 -0
  5. data/app/views/settings/_avm.html.erb +49 -0
  6. data/config/initializers/000_dependencies.rb +6 -0
  7. data/config/initializers/events_manager.rb +17 -0
  8. data/config/initializers/redmine_plugins_helper.rb +22 -0
  9. data/config/locales/en.yml +10 -0
  10. data/config/locales/pt-BR.yml +10 -0
  11. data/config/routes.rb +5 -0
  12. data/db/migrate/20160516164053_create_issue_status_assigns.rb +10 -0
  13. data/db/migrate/20180711164719_rename_plugin_avm_to_redmine_avm.rb +40 -0
  14. data/init.rb +21 -0
  15. data/lib/avm/issue/assign.rb +49 -0
  16. data/lib/avm/issue/dependencies_section_check.rb +58 -0
  17. data/lib/avm/issue/motivation_check.rb +33 -0
  18. data/lib/avm/issue/unblock.rb +45 -0
  19. data/lib/avm/issue/undefine.rb +47 -0
  20. data/lib/avm/listeners/issue_auto_assign.rb +25 -0
  21. data/lib/avm/listeners/issue_auto_unblock.rb +36 -0
  22. data/lib/avm/listeners/issue_auto_unblock/issue_delete.rb +21 -0
  23. data/lib/avm/listeners/issue_auto_unblock/issue_relation_delete.rb +22 -0
  24. data/lib/avm/listeners/issue_auto_unblock/issue_update.rb +45 -0
  25. data/lib/avm/listeners/issue_auto_undefine.rb +37 -0
  26. data/lib/avm/listeners/issue_auto_undefine/issue_create.rb +23 -0
  27. data/lib/avm/listeners/issue_auto_undefine/issue_relation_create.rb +23 -0
  28. data/lib/avm/listeners/issue_auto_undefine/issue_update.rb +52 -0
  29. data/lib/avm/listeners/issue_dependencies_section_check.rb +29 -0
  30. data/lib/avm/listeners/issue_motivation_check.rb +29 -0
  31. data/lib/avm/patches/issue_patch.rb +74 -0
  32. data/lib/avm/patches/test_case_patch.rb +26 -0
  33. data/lib/avm/settings.rb +64 -0
  34. data/lib/redmine_avm.rb +5 -0
  35. data/lib/redmine_avm/version.rb +8 -0
  36. data/lib/tasks/issue.rake +54 -0
  37. data/lib/tasks/redmine_avm.rake +13 -0
  38. metadata +98 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 491c7e0c46f51ace5676cfbb1025bb731ab26ad3ec4814763cf66a6ec06d9c3b
4
+ data.tar.gz: b051350b016dcf1055111222f7b2c83807ecf9a4c35ddc553623333665f0b73d
5
+ SHA512:
6
+ metadata.gz: acf3a7957aad165d003a34c0333b62fb3c71c4a6d59cbbd4ea6e7bcc8693223adc3d9869f17a67a6c9e1d70d8decb77c9c92545795dbe04369ef54572598405a
7
+ data.tar.gz: cbbf26130460b0dccbe194cee629177eab52f696089e0b3ae3445f3eef2ed203a50989de2114665a59837e79fb08b051f937fb6141586da3d3bea6defe2a63b1
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IssueStatusAssignsController < ApplicationController
4
+ before_action :require_admin, :active_scaffold_set_dynamic_options
5
+ layout 'admin'
6
+
7
+ active_scaffold :issue_status_assign do |conf|
8
+ conf.columns[:issue_status].form_ui = :select
9
+ conf.columns[:issue_field].form_ui = :select
10
+ end
11
+
12
+ private
13
+
14
+ def active_scaffold_set_dynamic_options
15
+ active_scaffold_config.columns[:issue_field].options =
16
+ { options: issue_field_options }
17
+ end
18
+
19
+ def issue_field_options
20
+ CustomField.where(type: 'IssueCustomField', field_format: 'user')
21
+ .map { |cf| [cf.id, cf.to_s] }
22
+ end
23
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IssueStatusAssignsHelper
4
+ def options_for_association_conditions(association, record)
5
+ if association.name == :issue_field
6
+ { type: 'IssueCustomField', field_format: 'user' }
7
+ else
8
+ super
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class IssueStatusAssign < ActiveRecord::Base
4
+ belongs_to :issue_status, dependent: :destroy
5
+ belongs_to :issue_field, class_name: 'CustomField', dependent: :destroy
6
+
7
+ validates :issue_status, presence: true, uniqueness: true
8
+ validates :issue_field, presence: true
9
+ end
@@ -0,0 +1,49 @@
1
+ <p>
2
+ <label>Usuário administrador</label>
3
+ <select name="settings[admin_user_id]">
4
+ <%= options_for_select(User.all.order('firstname','lastname').collect {|u| [ u.to_s, u.id ] }, settings['admin_user_id'] ) %>
5
+ </select>
6
+ </p>
7
+ <p>
8
+ <label>Situação de tarefa indefinda.</label>
9
+ <select name="settings[issue_status_undefined_id]">
10
+ <%= options_for_select([['-','']] + IssueStatus.all.collect {|p| [ p.name, p.id ] }, settings['issue_status_undefined_id'] ) %>
11
+ </select>
12
+ </p>
13
+ <p>
14
+ <label>Situação de tarefa bloqueada.</label>
15
+ <select name="settings[issue_status_blocked_id]">
16
+ <%= options_for_select([['-','']] + IssueStatus.all.collect {|p| [ p.name, p.id ] }, settings['issue_status_blocked_id'] ) %>
17
+ </select>
18
+ </p>
19
+ <p>
20
+ <label>Situação de tarefa desbloqueada.</label>
21
+ <select name="settings[issue_status_unblocked_id]">
22
+ <%= options_for_select([['-','']] + IssueStatus.all.collect {|p| [ p.name, p.id ] }, settings['issue_status_unblocked_id'] ) %>
23
+ </select>
24
+ </p>
25
+ <p>
26
+ <label>Título da seção de dependências.</label>
27
+ <%= text_field_tag 'settings[dependencies_section_title]',
28
+ settings['dependencies_section_title'] %>
29
+ </p>
30
+ <p>
31
+ <label>Mensagem para seção de dependências não encontrada.</label>
32
+ <%= text_area_tag 'settings[no_dependencies_section_message]',
33
+ settings['no_dependencies_section_message'] %>
34
+ </p>
35
+ <p>
36
+ <label>Mensagem para dependências ausentes na seção de dependências.</label>
37
+ <%= text_area_tag 'settings[dependencies_section_missing_dependencies_message]',
38
+ settings['dependencies_section_missing_dependencies_message'] %>
39
+ </p>
40
+ <p>
41
+ <label>Título da seção de motivação.</label>
42
+ <%= text_field_tag 'settings[motivation_section_title]',
43
+ settings['motivation_section_title'] %>
44
+ </p>
45
+ <p>
46
+ <label>Mensagem indefinição por imotivação.</label>
47
+ <%= text_area_tag 'settings[unmotivated_message]',
48
+ settings['unmotivated_message'] %>
49
+ </p>
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Redmine::Plugin.post_register :redmine_avm do
4
+ # Source: https://github.com/esquilo-azul/redmine_events_manager
5
+ requires_redmine_plugin(:redmine_events_manager, version_or_higher: '0.4.2')
6
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ EventsManager.add_listener(Issue, :create, 'Avm::Listeners::IssueAutoUndefine')
4
+ EventsManager.add_listener(Issue, :update, 'Avm::Listeners::IssueAutoUndefine')
5
+ EventsManager.add_listener(IssueRelation, :create, 'Avm::Listeners::IssueAutoUndefine')
6
+ EventsManager.add_listener(IssueRelation, :delete, 'Avm::Listeners::IssueAutoUnblock')
7
+ EventsManager.add_listener(Issue, :delete, 'Avm::Listeners::IssueAutoUnblock')
8
+ EventsManager.add_listener(Issue, :update, 'Avm::Listeners::IssueAutoUnblock')
9
+ EventsManager.add_listener(Issue, :create, 'Avm::Listeners::IssueAutoAssign')
10
+ EventsManager.add_listener(Issue, :update, 'Avm::Listeners::IssueAutoAssign')
11
+ EventsManager.add_listener(Issue, :update,
12
+ 'Avm::Listeners::IssueDependenciesSectionCheck')
13
+ EventsManager.add_listener(IssueRelation, :create,
14
+ 'Avm::Listeners::IssueDependenciesSectionCheck')
15
+ [[Issue, :create], [Issue, :update], [IssueRelation, :create]].each do |v|
16
+ EventsManager.add_listener(v.first, v.last, 'Avm::Listeners::IssueMotivationCheck')
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ default_value = {
4
+ dependencies_section_title: 'Dependencies',
5
+ no_dependencies_section_message: <<MESSAGE.strip_heredoc,
6
+ Dependencies section not found.
7
+ Customize this message in plugin AVM's configuration.
8
+ MESSAGE
9
+ dependencies_section_missing_dependencies_message: <<MESSAGE.strip_heredoc,
10
+ Missing dependencies found dependencies section: %{ids}.
11
+ Customize this message in plugin AVM's configuration.
12
+ MESSAGE
13
+ motivation_section_title: 'Motivation',
14
+ unmotivated_message: 'This issue is not motivated.'
15
+ }
16
+
17
+ if ActiveRecord::Base.connection.table_exists? ::User.table_name
18
+ default_admin = ::User.where(id: 1, admin: true).first
19
+ default_value[:admin_user_id] = default_admin.id if default_admin
20
+ end
21
+
22
+ ::RedminePluginsHelper::Settings.default(:redmine_avm, default_value)
@@ -0,0 +1,10 @@
1
+ en:
2
+ issue_assign_message: |
3
+ Assigned to *%{user}* because:
4
+
5
+ # The issue's status is *%{issue_status}*;
6
+ # For this status the expected role is *%{issue_field}*;
7
+ # *%{user}* is the issue's *%{issue_field}*.
8
+ issue_unblock_message: 'This issue was unblocked because has no open dependencies.'
9
+ issue_undefine_message: 'This issue was undefined because it has undefined dependencies: %{undefined_dependencies_ids}.'
10
+ label_issue_status_assigns: Issue status assigns
@@ -0,0 +1,10 @@
1
+ pt-BR:
2
+ issue_assign_message: |
3
+ Atribuído para *%{user}* porque:
4
+
5
+ # A tarefa está na situação *%{issue_status}*;
6
+ # Nessa situação o responsável esperado é o *%{issue_field}*;
7
+ # *%{user}* é o *%{issue_field}* da tarefa.
8
+ issue_unblock_message: 'Esta tarefa foi desbloqueada porque não possui dependências abertas.'
9
+ issue_undefine_message: 'Esta tarefa foi indefinida porque possui dependências indefinidas: %{undefined_dependencies_ids}.'
10
+ label_issue_status_assigns: Atribuição por situações de tarefa
data/config/routes.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ RedmineApp::Application.routes.draw do
4
+ resources(:issue_status_assigns) { as_routes }
5
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateIssueStatusAssigns < ActiveRecord::Migration
4
+ def change
5
+ create_table :issue_status_assigns do |t|
6
+ t.references :issue_status
7
+ t.references :issue_field
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RenamePluginAvmToRedmineAvm < ActiveRecord::Migration
4
+ class Settings < ActiveRecord::Base
5
+ end
6
+
7
+ def up
8
+ rename_plugin('avm', 'redmine_avm')
9
+ end
10
+
11
+ def down
12
+ rename_plugin('redmine_avm', 'avm')
13
+ end
14
+
15
+ private
16
+
17
+ def rename_plugin(from, to)
18
+ old_record = Setting.find_by(name: from)
19
+ return if old_record.blank?
20
+
21
+ new_record = Setting.find_by(name: to)
22
+ if new_record.present?
23
+ new_value = plugin_setting_value(old_record).value.merge(plugin_setting_value(new_record))
24
+ new_record.update!(value: new_value)
25
+ old_record.destroy!
26
+ else
27
+ old_record.update!(name: to)
28
+ end
29
+ end
30
+
31
+ def plugin_setting_value(record)
32
+ if record.value.blank?
33
+ {}.with_indifferent_access
34
+ elsif record.value.is_a?(Hash)
35
+ record.value
36
+ else
37
+ raise "#{record} value is not Hash or blank"
38
+ end
39
+ end
40
+ end
data/init.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redmine'
4
+
5
+ require 'avm/patches/issue_patch'
6
+ require 'avm/patches/test_case_patch'
7
+
8
+ Redmine::Plugin.register :redmine_avm do
9
+ name 'Redmine\'s AVM plugin'
10
+ author ::RedmineNonprojectModules::AUTHOR
11
+ description ::RedmineAvm::SUMMARY
12
+ version ::RedmineNonprojectModules::VERSION
13
+ url ::RedmineNonprojectModules::HOMEPAGE
14
+
15
+ settings(partial: 'settings/avm')
16
+
17
+ Redmine::MenuManager.map :admin_menu do |menu|
18
+ menu.push :issue_status_assigns, { controller: 'issue_status_assigns', action: 'index' },
19
+ caption: :label_issue_status_assigns
20
+ end
21
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Issue
5
+ class Assign
6
+ attr_reader :issue
7
+
8
+ def initialize(issue)
9
+ @issue = issue
10
+ end
11
+
12
+ def run
13
+ return unless issue_status_assign && issue_has_assign_field?
14
+ return unless issue.assigned_to || issue_status_user
15
+ return if issue.assigned_to == issue_status_user
16
+
17
+ assign
18
+ end
19
+
20
+ private
21
+
22
+ def issue_status_assign
23
+ @issue_status_assign ||= IssueStatusAssign.where(issue_status: issue.status).first
24
+ end
25
+
26
+ def issue_has_assign_field?
27
+ issue.available_custom_fields.include?(issue_status_assign.issue_field)
28
+ end
29
+
30
+ def issue_status_user
31
+ id = issue.custom_field_value(issue_status_assign.issue_field)
32
+ id.present? ? User.find(id) : nil
33
+ end
34
+
35
+ def assign
36
+ user = issue_status_user
37
+ Rails.logger.debug("Assigning #{user} to #{issue}")
38
+ user_label = user ? user.to_s : 'NENHUM'
39
+ @issue.init_journal(
40
+ Avm::Settings.admin_user,
41
+ I18n.translate(:issue_assign_message, user: user_label, issue_status: issue.status,
42
+ issue_field: issue_status_assign.issue_field.name)
43
+ )
44
+ @issue.assigned_to = user
45
+ @issue.save!
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Issue
5
+ class DependenciesSectionCheck
6
+ def initialize(issue)
7
+ @issue = issue
8
+ end
9
+
10
+ def run
11
+ return if @issue.closed?
12
+ return if @issue.dependencies.empty?
13
+
14
+ unless @issue.dependencies_section
15
+ Rails.logger.info("\##{@issue.id}: no section found")
16
+ undefine_no_dependencies_secion
17
+ return
18
+ end
19
+ return if all_dependencies_in_dependencies_section?
20
+
21
+ Rails.logger.info("\##{@issue.id}: missing dependencies in dependency section")
22
+ undefine_no_all_dependencies
23
+ end
24
+
25
+ private
26
+
27
+ def all_dependencies_in_dependencies_section?
28
+ sec_deps = @issue.dependencies_section_dependencies
29
+ @issue.dependencies.all? { |d| sec_deps.include?(d.id) }
30
+ end
31
+
32
+ def undefine_no_dependencies_secion
33
+ @issue.init_journal(
34
+ Avm::Settings.admin_user,
35
+ Avm::Settings.no_dependencies_section_message
36
+ )
37
+ @issue.status = Avm::Settings.issue_status_undefined
38
+ @issue.save!
39
+ end
40
+
41
+ def undefine_no_all_dependencies
42
+ @issue.init_journal(
43
+ Avm::Settings.admin_user,
44
+ format(Avm::Settings.dependencies_section_missing_dependencies_message,
45
+ ids: no_dependencies_section_ids_string)
46
+ )
47
+ @issue.status = Avm::Settings.issue_status_undefined
48
+ @issue.save!
49
+ end
50
+
51
+ def no_dependencies_section_ids_string
52
+ sec_deps = @issue.dependencies_section_dependencies
53
+ deps = @issue.dependencies.map(&:id)
54
+ (deps - sec_deps).map { |id| "##{id}" }.join(', ')
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Issue
5
+ class MotivationCheck
6
+ def initialize(issue)
7
+ @issue = issue
8
+ end
9
+
10
+ def run
11
+ return if motivated?
12
+
13
+ Rails.logger.info("\##{@issue.id}: unmotivated")
14
+ undefine
15
+ end
16
+
17
+ private
18
+
19
+ def motivated?
20
+ @issue.closed? || @issue.motivated?
21
+ end
22
+
23
+ def undefine
24
+ @issue.init_journal(
25
+ Avm::Settings.admin_user,
26
+ Avm::Settings.unmotivated_message
27
+ )
28
+ @issue.status = Avm::Settings.issue_status_undefined
29
+ @issue.save!
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Issue
5
+ class Unblock
6
+ def initialize(issue)
7
+ @issue = issue
8
+ end
9
+
10
+ def run
11
+ msg = unblock?
12
+ if msg
13
+ Rails.logger.debug(msg)
14
+ else
15
+ Rails.logger.debug("##{@issue.id} has no open dependencies. Unblocking...")
16
+ unblock
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def unblock?
23
+ return "##{@issue.id} is not blocked" unless status_blocked?
24
+ return "##{@issue.id} has open dependencies" if open_dependencies?
25
+ end
26
+
27
+ def status_blocked?
28
+ @issue.status == Avm::Settings.issue_status_blocked
29
+ end
30
+
31
+ def open_dependencies?
32
+ @issue.dependencies.any? { |d| !d.closed? }
33
+ end
34
+
35
+ def unblock
36
+ @issue.init_journal(
37
+ Avm::Settings.admin_user,
38
+ I18n.translate(:issue_unblock_message)
39
+ )
40
+ @issue.status = Avm::Settings.issue_status_unblocked
41
+ @issue.save!
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Issue
5
+ class Undefine
6
+ def initialize(issue)
7
+ @issue = issue
8
+ end
9
+
10
+ def run
11
+ msg = undefine?
12
+ if msg
13
+ Rails.logger.debug(msg)
14
+ else
15
+ Rails.logger.debug("##{@issue.id} has undefined dependencies. Undefining...")
16
+ undefine
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def undefine?
23
+ return "##{@issue.id} already undefined" if @issue.undefined?
24
+ return "##{@issue.id} already closed" if @issue.closed?
25
+ return "##{@issue.id} has no undefined dependencies" unless @issue.dependencies.any?(
26
+ &:undefined?
27
+ )
28
+ end
29
+
30
+ def undefine
31
+ @issue.init_journal(
32
+ Avm::Settings.admin_user,
33
+ I18n.translate(
34
+ :issue_undefine_message,
35
+ undefined_dependencies_ids: undefined_dependency_ids_string
36
+ )
37
+ )
38
+ @issue.status = Avm::Settings.issue_status_undefined
39
+ @issue.save!
40
+ end
41
+
42
+ def undefined_dependency_ids_string
43
+ @issue.dependencies.select(&:undefined?).map { |i| "##{i.id}" }.join(', ')
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoAssign
6
+ attr_reader :event
7
+
8
+ def initialize(event)
9
+ @event = event
10
+ end
11
+
12
+ def run
13
+ issue = issue_to_check
14
+ Avm::Issue::Assign.new(issue).run if issue
15
+ end
16
+
17
+ private
18
+
19
+ def issue_to_check
20
+ return event.data if event.issue_create?
21
+ return event.data.issue if event.issue_update?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUnblock
6
+ include IssueDelete
7
+ include IssueRelationDelete
8
+ include IssueUpdate
9
+
10
+ attr_reader :event
11
+
12
+ def initialize(event)
13
+ @event = event
14
+ end
15
+
16
+ def run
17
+ return if check_conditions
18
+
19
+ Rails.logger.debug("Unblock condition not found for #{@event}")
20
+ end
21
+
22
+ def check_conditions
23
+ %w[issue_relation_deleted issue_deleted issue_updated].any? do |m|
24
+ issues = send(m)
25
+ next unless issues
26
+
27
+ issues.each do |issue|
28
+ Rails.logger.debug("#{m}: #{issue}")
29
+ Avm::Issue::Unblock.new(issue).run
30
+ end
31
+ true
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUnblock
6
+ module IssueDelete
7
+ def issue_deleted
8
+ return unless event.entity == ::Issue && event.action == :delete
9
+
10
+ ::Issue.where(status: Avm::Settings.issue_status_blocked)
11
+ end
12
+
13
+ private
14
+
15
+ def issue
16
+ event.data
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUnblock
6
+ module IssueRelationDelete
7
+ def issue_relation_deleted
8
+ return unless event.entity == IssueRelation && event.action == :delete
9
+ return unless relation.relation_type == IssueRelation::TYPE_BLOCKS
10
+
11
+ [relation.issue_to]
12
+ end
13
+
14
+ private
15
+
16
+ def relation
17
+ event.data
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUnblock
6
+ module IssueUpdate
7
+ def issue_updated
8
+ return unless issue_update_event?
9
+ return [issue] if status_changed_to_blocked?
10
+ return issue.r_dependencies if status_changed_to_closed?
11
+ end
12
+
13
+ private
14
+
15
+ def issue_update_event?
16
+ event.entity == ::Issue && event.action == :update
17
+ end
18
+
19
+ def journal
20
+ event.data
21
+ end
22
+
23
+ def issue
24
+ journal.issue
25
+ end
26
+
27
+ def status_changed_to_blocked?
28
+ new_integer_value('status_id') == Avm::Settings.issue_status_blocked.id
29
+ end
30
+
31
+ def new_integer_value(attr)
32
+ d = journal.detail_for_attribute(attr)
33
+ return nil unless d
34
+
35
+ d.value.to_i
36
+ end
37
+
38
+ def status_changed_to_closed?
39
+ sid = new_integer_value('status_id')
40
+ sid ? IssueStatus.find(sid).is_closed : false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUndefine
6
+ include IssueCreate
7
+ include IssueUpdate
8
+ include IssueRelationCreate
9
+
10
+ attr_reader :event
11
+
12
+ def initialize(event)
13
+ @event = event
14
+ end
15
+
16
+ def run
17
+ return if check_conditions
18
+
19
+ Rails.logger.debug("Undefine condition not found for #{@event}")
20
+ end
21
+
22
+ def check_conditions
23
+ %w[issue_created_undefined issue_updated_undefined
24
+ issue_relation_created_undefined].any? do |m|
25
+ issues = send(m)
26
+ next unless issues
27
+
28
+ issues.each do |issue|
29
+ Rails.logger.debug("#{m}: #{issue}")
30
+ Avm::Issue::Undefine.new(issue).run
31
+ end
32
+ true
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUndefine
6
+ module IssueCreate
7
+ def issue_created_undefined
8
+ return unless event.entity == Issue && event.action == :create
9
+ return unless issue.status == Avm::Settings.issue_status_undefined
10
+ return unless issue.parent
11
+
12
+ [issue.parent]
13
+ end
14
+
15
+ private
16
+
17
+ def issue
18
+ event.data
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUndefine
6
+ module IssueRelationCreate
7
+ def issue_relation_created_undefined
8
+ return unless @event.entity == IssueRelation && @event.action == :create
9
+ return unless relation.relation_type == IssueRelation::TYPE_BLOCKS
10
+ return unless relation.issue_from.status == Avm::Settings.issue_status_undefined
11
+
12
+ [relation.issue_to]
13
+ end
14
+
15
+ private
16
+
17
+ def relation
18
+ event.data
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueAutoUndefine
6
+ module IssueUpdate
7
+ def issue_updated_undefined
8
+ return unless issue_update_event?
9
+ return journal.issue.r_dependencies if status_changed_to_undefined?
10
+ return [journal.issue] if status_changed_from_undefined?
11
+ return [journal.issue.parent] if parent_changed_and_undefined?
12
+ end
13
+
14
+ private
15
+
16
+ def issue_update_event?
17
+ @event.entity == Issue && @event.action == :update
18
+ end
19
+
20
+ def journal
21
+ @event.data
22
+ end
23
+
24
+ def status_changed_to_undefined?
25
+ new_integer_value('status_id') == Avm::Settings.issue_status_undefined.id
26
+ end
27
+
28
+ def status_changed_from_undefined?
29
+ old_integer_value('status_id') == Avm::Settings.issue_status_undefined.id
30
+ end
31
+
32
+ def parent_changed_and_undefined?
33
+ new_integer_value('parent_id') && journal.issue.undefined?
34
+ end
35
+
36
+ def new_integer_value(attr)
37
+ d = journal.detail_for_attribute(attr)
38
+ return nil unless d
39
+
40
+ d.value.to_i
41
+ end
42
+
43
+ def old_integer_value(attr)
44
+ d = journal.detail_for_attribute(attr)
45
+ return nil unless d
46
+
47
+ d.old_value.to_i
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueDependenciesSectionCheck
6
+ attr_reader :event
7
+
8
+ def initialize(event)
9
+ @event = event
10
+ end
11
+
12
+ def run
13
+ issue = issue_to_check
14
+ return unless issue
15
+ return if issue.status == Avm::Settings.issue_status_undefined
16
+
17
+ Avm::Issue::DependenciesSectionCheck.new(issue).run if issue
18
+ end
19
+
20
+ private
21
+
22
+ def issue_to_check
23
+ return event.data.issue if event.issue_update?
24
+ return event.data.issue_to if event.issue_relation_create? &&
25
+ event.data.relation_type == IssueRelation::TYPE_BLOCKS
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Listeners
5
+ class IssueMotivationCheck
6
+ attr_reader :event
7
+
8
+ def initialize(event)
9
+ @event = event
10
+ end
11
+
12
+ def run
13
+ issue = issue_to_check
14
+ return unless issue
15
+ return if issue.status == Avm::Settings.issue_status_undefined
16
+
17
+ Avm::Issue::MotivationCheck.new(issue).run if issue
18
+ end
19
+
20
+ private
21
+
22
+ def issue_to_check
23
+ return event.data.issue if event.issue_update?
24
+ return event.data.issue_to if event.issue_relation_create? &&
25
+ event.data.relation_type == IssueRelation::TYPE_BLOCKS
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Patches
5
+ module IssuePatch
6
+ def self.included(base)
7
+ base.send(:include, InstanceMethods)
8
+ base.send(:include, MotivationMethods)
9
+ end
10
+
11
+ module InstanceMethods
12
+ def undefined?
13
+ status == ::Avm::Settings.issue_status_undefined
14
+ end
15
+
16
+ def description_section(section_title)
17
+ ['h\1\.', '----', '$'].each do |e|
18
+ m = /h([0-9])\.\s*#{Regexp.escape(section_title)}(.+)#{e}/m
19
+ .match(description)
20
+ return m[2].strip + "\r\n" if m
21
+ end
22
+ nil
23
+ end
24
+
25
+ def dependencies
26
+ children.all + relations_to.select do |r|
27
+ r.relation_type == ::IssueRelation::TYPE_BLOCKS
28
+ end.map(&:issue_from)
29
+ end
30
+
31
+ def r_dependencies
32
+ rd = []
33
+ rd << parent if parent
34
+ rd + relations_from.select do |r|
35
+ r.relation_type == ::IssueRelation::TYPE_BLOCKS
36
+ end.map(&:issue_to)
37
+ end
38
+
39
+ def dependencies_section
40
+ description_section(::Avm::Settings.dependencies_section_title)
41
+ end
42
+
43
+ def dependencies_section_dependencies
44
+ s = dependencies_section
45
+ return [] unless s
46
+
47
+ s.scan(/\#([0-9]+)/).map { |x| x[0].to_i }
48
+ end
49
+ end
50
+
51
+ module MotivationMethods
52
+ def motivated?
53
+ motivated_by_self? || motivated_by_relations?
54
+ end
55
+
56
+ def motivated_by_self?
57
+ !undefined? && motivation_section.present?
58
+ end
59
+
60
+ def motivated_by_relations?
61
+ !undefined? && r_dependencies.any?(&:motivated?)
62
+ end
63
+
64
+ def motivation_section
65
+ description_section(::Avm::Settings.motivation_section_title)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ unless ::Issue.included_modules.include? ::Avm::Patches::IssuePatch
73
+ ::Issue.send(:include, ::Avm::Patches::IssuePatch)
74
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ module Patches
5
+ module TestCasePatch
6
+ def self.included(base)
7
+ base.setup do
8
+ s = ::Setting.plugin_redmine_avm.dup || {}
9
+ s['issue_status_undefined_id'] = 1
10
+ s['issue_status_blocked_id'] = 4
11
+ s['issue_status_unblocked_id'] = 2
12
+ s['admin_user_id'] = 1
13
+ ::Setting.plugin_redmine_avm = s
14
+ ::ListenerOption.listener_enable('Avm::Listeners::IssueMotivationCheck', false)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ if Rails.env.test?
22
+ require Rails.root.join('test', 'test_helper.rb')
23
+ unless ::ActiveSupport::TestCase.included_modules.include? Avm::Patches::TestCasePatch
24
+ ::ActiveSupport::TestCase.send(:include, Avm::Patches::TestCasePatch)
25
+ end
26
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Avm
4
+ class Settings
5
+ class << self
6
+ def issue_status_undefined(raise_if_empty = true)
7
+ issue_status('issue_status_undefined_id', 'indefida', raise_if_empty)
8
+ end
9
+
10
+ def issue_status_blocked(raise_if_empty = true)
11
+ issue_status('issue_status_blocked_id', 'bloqueada', raise_if_empty)
12
+ end
13
+
14
+ def issue_status_unblocked(raise_if_empty = true)
15
+ issue_status('issue_status_unblocked_id', 'desbloqueada', raise_if_empty)
16
+ end
17
+
18
+ def admin_user(raise_if_empty = true)
19
+ if Setting.plugin_redmine_avm['admin_user_id'].present?
20
+ User.find(Setting.plugin_redmine_avm['admin_user_id'])
21
+ elsif raise_if_empty
22
+ raise 'Usuário administrador não configurado. Acesse /settings/plugin/avm.'
23
+ end
24
+ end
25
+
26
+ def dependencies_section_title
27
+ required_text(__method__)
28
+ end
29
+
30
+ def no_dependencies_section_message
31
+ required_text(__method__)
32
+ end
33
+
34
+ def dependencies_section_missing_dependencies_message
35
+ required_text(__method__)
36
+ end
37
+
38
+ def motivation_section_title
39
+ required_text(__method__)
40
+ end
41
+
42
+ def unmotivated_message
43
+ required_text(__method__)
44
+ end
45
+
46
+ private
47
+
48
+ def issue_status(key, message, raise_if_empty)
49
+ if Setting.plugin_redmine_avm[key].present?
50
+ IssueStatus.find(Setting.plugin_redmine_avm[key])
51
+ elsif raise_if_empty
52
+ raise "Situação de tarefa #{message} não configurada. Acesse /settings/plugin/avm."
53
+ end
54
+ end
55
+
56
+ def required_text(key)
57
+ v = Setting.plugin_redmine_avm[key]
58
+ return v if v.present?
59
+
60
+ raise "Setting.plugin_redmine_avm[#{key}] is empty"
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedmineAvm
4
+ require 'redmine_avm/version'
5
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedmineAvm
4
+ AUTHOR = 'Eduardo Henrique Bogoni'
5
+ HOMEPAGE = 'https://github.com/esquilo-azul/redmine_avm'
6
+ SUMMARY = 'AVM support for Redmine.'
7
+ VERSION = '0.3.3'
8
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :avm do
4
+ namespace :issue do
5
+ desc 'Verifica auto-indefinição de uma tarefa'
6
+ task :undefine, [:issue_id] => :environment do |_t, args|
7
+ Avm::Issue::Undefine.new(Issue.find(args.issue_id)).run
8
+ end
9
+
10
+ desc 'Verifica auto-indefinição de todas as tarefas indefinidas'
11
+ task undefine_all: :environment do |_t, _args|
12
+ Issue.where.not(status: Avm::Settings.issue_status_undefined).each do |i|
13
+ Avm::Issue::Undefine.new(i).run
14
+ end
15
+ end
16
+
17
+ desc 'Verifica auto-desbloqueio de uma tarefa'
18
+ task unblock: :environment do |_t, _args|
19
+ Issue.where(status: Avm::Settings.issue_status_blocked).each do |i|
20
+ Avm::Issue::Unblock.new(i).run
21
+ end
22
+ end
23
+
24
+ desc 'Verifica auto-desbloqueio de todas as tarefas'
25
+ task unblock_all: :environment do |_t, _args|
26
+ Issue.where(status: Avm::Settings.issue_status_blocked).each do |i|
27
+ Avm::Issue::Unblock.new(i).run
28
+ end
29
+ end
30
+
31
+ desc 'Verifica auto-atribuição de todas as tarefas'
32
+ task assign_all: :environment do |_t, _args|
33
+ Issue.all.each do |i|
34
+ Avm::Issue::Assign.new(i).run
35
+ end
36
+ end
37
+
38
+ desc 'Verifica a seção de dependências de todas as tarefas'
39
+ task dependencies_section_check_all: :environment do |_t, _args|
40
+ Issue.where(closed_on: nil)
41
+ .where.not(status: Avm::Settings.issue_status_undefined).each do |i|
42
+ Avm::Issue::DependenciesSectionCheck.new(i).run
43
+ end
44
+ end
45
+
46
+ desc 'Verifica a ausência de motivação em todas as tarefas'
47
+ task motivation_check_all: :environment do |_t, _args|
48
+ Issue.where(closed_on: nil)
49
+ .where.not(status: Avm::Settings.issue_status_undefined).each do |i|
50
+ Avm::Issue::MotivationCheck.new(i).run
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :redmine_avm do
4
+ Rake::TestTask.new(test: 'db:test:prepare') do |t|
5
+ plugin_root = ::File.dirname(::File.dirname(__dir__))
6
+
7
+ t.description = 'Run plugin redmine_avm\'s tests.'
8
+ t.libs << 'test'
9
+ t.test_files = FileList["#{plugin_root}/test/**/*_test.rb"]
10
+ t.verbose = false
11
+ t.warning = false
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redmine_avm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.3
5
+ platform: ruby
6
+ authors:
7
+ - Eduardo Henrique Bogoni
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aranha
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.10'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.10.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '0.10'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 0.10.1
33
+ description:
34
+ email:
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - app/controllers/issue_status_assigns_controller.rb
40
+ - app/helpers/issue_status_assigns_helper.rb
41
+ - app/models/issue_status_assign.rb
42
+ - app/views/settings/_avm.html.erb
43
+ - config/initializers/000_dependencies.rb
44
+ - config/initializers/events_manager.rb
45
+ - config/initializers/redmine_plugins_helper.rb
46
+ - config/locales/en.yml
47
+ - config/locales/pt-BR.yml
48
+ - config/routes.rb
49
+ - db/migrate/20160516164053_create_issue_status_assigns.rb
50
+ - db/migrate/20180711164719_rename_plugin_avm_to_redmine_avm.rb
51
+ - init.rb
52
+ - lib/avm/issue/assign.rb
53
+ - lib/avm/issue/dependencies_section_check.rb
54
+ - lib/avm/issue/motivation_check.rb
55
+ - lib/avm/issue/unblock.rb
56
+ - lib/avm/issue/undefine.rb
57
+ - lib/avm/listeners/issue_auto_assign.rb
58
+ - lib/avm/listeners/issue_auto_unblock.rb
59
+ - lib/avm/listeners/issue_auto_unblock/issue_delete.rb
60
+ - lib/avm/listeners/issue_auto_unblock/issue_relation_delete.rb
61
+ - lib/avm/listeners/issue_auto_unblock/issue_update.rb
62
+ - lib/avm/listeners/issue_auto_undefine.rb
63
+ - lib/avm/listeners/issue_auto_undefine/issue_create.rb
64
+ - lib/avm/listeners/issue_auto_undefine/issue_relation_create.rb
65
+ - lib/avm/listeners/issue_auto_undefine/issue_update.rb
66
+ - lib/avm/listeners/issue_dependencies_section_check.rb
67
+ - lib/avm/listeners/issue_motivation_check.rb
68
+ - lib/avm/patches/issue_patch.rb
69
+ - lib/avm/patches/test_case_patch.rb
70
+ - lib/avm/settings.rb
71
+ - lib/redmine_avm.rb
72
+ - lib/redmine_avm/version.rb
73
+ - lib/tasks/issue.rake
74
+ - lib/tasks/redmine_avm.rake
75
+ homepage: https://github.com/esquilo-azul/redmine_avm
76
+ licenses: []
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.7.7
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: AVM support for Redmine.
98
+ test_files: []