redmine_avm 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []