redmine_events_manager 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.avm.yml +3 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +40 -0
  5. data/SelfGemfile +5 -0
  6. data/app/controllers/event_exceptions_controller.rb +32 -0
  7. data/app/controllers/listener_options_controller.rb +12 -0
  8. data/app/models/event_exception.rb +8 -0
  9. data/app/models/listener_option.rb +37 -0
  10. data/assets/stylesheets/application.css +8 -0
  11. data/bin/bundle +5 -0
  12. data/config/initializers/000_dependencies.rb +6 -0
  13. data/config/initializers/assets.rb +0 -0
  14. data/config/locales/pt-BR.yml +7 -0
  15. data/config/routes.rb +11 -0
  16. data/db/migrate/20160509152749_create_delayed_jobs.rb +27 -0
  17. data/db/migrate/20160717222418_create_event_exceptions.rb +20 -0
  18. data/db/migrate/20180331122033_create_listener_options.rb +14 -0
  19. data/db/migrate/20180419202618_add_enabled_to_listener_options.rb +9 -0
  20. data/db/migrate/20180711170315_rename_plugin_events_manager_to_redmine_events_manager.rb +42 -0
  21. data/init.rb +37 -0
  22. data/lib/events_manager.rb +99 -0
  23. data/lib/events_manager/event.rb +29 -0
  24. data/lib/events_manager/hooks/add_assets.rb +11 -0
  25. data/lib/events_manager/patches/issue_patch.rb +32 -0
  26. data/lib/events_manager/patches/issue_relation_patch.rb +32 -0
  27. data/lib/events_manager/patches/journal_patch.rb +29 -0
  28. data/lib/events_manager/patches/repository/git_patch.rb +18 -0
  29. data/lib/events_manager/patches/test_case_patch.rb +17 -0
  30. data/lib/events_manager/patches/time_entry_patch.rb +39 -0
  31. data/lib/events_manager/removed_record.rb +22 -0
  32. data/lib/events_manager/settings.rb +17 -0
  33. data/lib/events_manager/updated_record.rb +14 -0
  34. data/lib/redmine_events_manager/test_config.rb +11 -0
  35. data/lib/redmine_events_manager/version.rb +8 -0
  36. data/lib/tasks/events.rake +46 -0
  37. data/lib/tasks/redmine_events_manager.rake +4 -0
  38. data/redmine_events_manager.gemspec +24 -0
  39. data/spec/controllers/event_exceptions_controller_spec.rb +18 -0
  40. data/spec/lib/events_manager_spec.rb +16 -0
  41. data/spec/models/event_exception_spec.rb +79 -0
  42. metadata +118 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a76fba8e0e80ac0cb125f39d3651721a0239b0e69ef46c818c0125b4eb9af2fd
4
+ data.tar.gz: 7f9f3742c930fb348878382b06a787077e88563f7d1cc3e430668db752537790
5
+ SHA512:
6
+ metadata.gz: 936392a8e1096dec12b8f309b32df278e59e511c085937de11f71ef8d88b718e877d5c2ae1c2c3ec2eb52f3553fbed851775b6b71ad70fe20a35e06942e2fe93
7
+ data.tar.gz: 4015c2642a34f911faa94428d1876a3d69ad1b847d3a974807b7f1df663d24bdc59be02e71fc761f994133733f1d637594bd5a05de850b8c338242d11c07189d
@@ -0,0 +1,3 @@
1
+ ruby:
2
+ rubocop:
3
+ gemfile: SelfGemfile
@@ -0,0 +1 @@
1
+ /SelfGemfile.lock
@@ -0,0 +1,40 @@
1
+ require:
2
+ - rubocop-rails
3
+ - rubocop-rspec
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.4
7
+ TargetRailsVersion: 4.2
8
+
9
+ Layout/LineLength:
10
+ Max: 100
11
+ Exclude:
12
+ - 'db/migrate/**/*'
13
+
14
+ Metrics/AbcSize:
15
+ Exclude:
16
+ - 'db/migrate/**/*'
17
+ - 'test/**/*'
18
+
19
+ Metrics/BlockLength:
20
+ Exclude:
21
+ - 'db/migrate/**/*'
22
+ - 'lib/tasks/**/*'
23
+ - 'spec/**/*.rb'
24
+
25
+ Metrics/MethodLength:
26
+ Exclude:
27
+ - 'db/migrate/**/*'
28
+ - 'test/**/*'
29
+
30
+ Style/Documentation:
31
+ Enabled: false
32
+
33
+ Style/HashEachMethods:
34
+ Enabled: true
35
+
36
+ Style/HashTransformKeys:
37
+ Enabled: true
38
+
39
+ Style/HashTransformValues:
40
+ Enabled: true
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EventExceptionsController < ApplicationController
4
+ before_action :require_admin
5
+ layout 'admin'
6
+ before_action :clear_event_exception_unchecked, only: :index # rubocop:disable Rails/LexicallyScopedActionFilter
7
+
8
+ active_scaffold :event_exception do |conf|
9
+ conf.actions.exclude :create, :update, :delete
10
+ conf.list.columns = %i[created_at event_entity event_action listener_class]
11
+ conf.list.sorting = { created_at: :desc }
12
+ conf.action_links.add :download, type: :member, label: 'Download', page: true
13
+ end
14
+
15
+ def download
16
+ ex = find_if_allowed(params[:id], :read)
17
+ send_data ex.attributes.to_yaml, filename: download_filename(ex)
18
+ end
19
+
20
+ private
21
+
22
+ def download_filename(error)
23
+ [Setting.host_name, 'exception', error.id.to_s, error.created_at.to_s].map do |s|
24
+ s.parameterize.strip
25
+ end.select(&:present?).join('_')
26
+ end
27
+
28
+ def clear_event_exception_unchecked
29
+ EventsManager::Settings.event_exception_unchecked = false if
30
+ EventsManager::Settings.event_exception_unchecked
31
+ end
32
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ListenerOptionsController < ApplicationController
4
+ before_action :require_admin
5
+ layout 'admin'
6
+
7
+ active_scaffold :listener_option do |conf|
8
+ conf.columns[:listener_class].form_ui = :select
9
+ conf.columns[:listener_class].options ||= {}
10
+ conf.columns[:listener_class].options[:options] = ::ListenerOption.listener_class_options
11
+ end
12
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class EventException < ActiveRecord::Base
4
+ self.inheritance_column = nil
5
+
6
+ validates :event_entity, :event_action, :event_data, :listener_class, :listener_instance,
7
+ :exception_class, :exception_message, :exception_stack, presence: true
8
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ListenerOption < ActiveRecord::Base
4
+ DEFAULT_DELAY = 0
5
+ DEFAULT_ENABLED = true
6
+
7
+ class << self
8
+ def listener_class_list
9
+ @listener_class_list ||= ::EventsManager.all_listeners
10
+ end
11
+
12
+ def listener_class_options
13
+ listener_class_list.sort.map { |v| [v, v] }
14
+ end
15
+
16
+ def listener_delay(listener_class)
17
+ o = ::ListenerOption.find_by(listener_class: listener_class.to_s)
18
+ o && o.delay.present? && o.delay >= 0 ? o.delay : DEFAULT_DELAY
19
+ end
20
+
21
+ def listener_enabled?(listener_class)
22
+ o = ::ListenerOption.find_by(listener_class: listener_class.to_s)
23
+ o.present? ? o.enabled? : DEFAULT_ENABLED
24
+ end
25
+
26
+ def listener_enable(listener_class, enabled)
27
+ o = ::ListenerOption.find_or_create_by(listener_class: listener_class.to_s)
28
+ o.enabled = enabled
29
+ o.save!
30
+ end
31
+ end
32
+
33
+ validates :listener_class, presence: true, uniqueness: true,
34
+ inclusion: { in: listener_class_list }
35
+ validates :delay, allow_blank: true,
36
+ numericality: { integer_only: true }
37
+ end
@@ -0,0 +1,8 @@
1
+ #top-menu .event-exception-unchecked {
2
+ background-image: url('../../../images/warning.png');
3
+ background-position: 0% 40%;
4
+ background-repeat: no-repeat;
5
+ padding-left: 20px;
6
+ padding-top: 2px;
7
+ padding-bottom: 3px;
8
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../SelfGemfile', __dir__)
5
+ load Gem.bin_path('bundler', 'bundle')
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Redmine::Plugin.post_register :redmine_events_manager do
4
+ # Source: https://github.com/esquilo-azul/redmine_nonproject_modules
5
+ requires_redmine_plugin(:redmine_nonproject_modules, version_or_higher: '0.2.1')
6
+ end
File without changes
@@ -0,0 +1,7 @@
1
+ pt-BR:
2
+ activerecord:
3
+ description:
4
+ listener_option:
5
+ delay: Atraso em segundos para o listener ser processado.
6
+ label_event_exception_plural: Exceções em eventos
7
+ label_listener_option_plural: Opções de listeners
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ RedmineApp::Application.routes.draw do
4
+ resources(:event_exceptions) do
5
+ as_routes
6
+ member do
7
+ get :download
8
+ end
9
+ end
10
+ resources(:listener_options) { as_routes }
11
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateDelayedJobs < (
4
+ Rails.version < '5.2' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
5
+ )
6
+ def self.up
7
+ create_table :delayed_jobs, force: true do |table|
8
+ table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of
9
+ # the queue
10
+ table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
11
+ table.text :handler, null: false # YAML-encoded string of the object that will do work
12
+ table.text :last_error # reason for last failure (See Note below)
13
+ table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
14
+ table.datetime :locked_at # Set when a client is working on this object
15
+ table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
16
+ table.string :locked_by # Who is working on this object (if locked)
17
+ table.string :queue # The name of the queue this job is in
18
+ table.timestamps null: true
19
+ end
20
+
21
+ add_index :delayed_jobs, %i[priority run_at], name: 'delayed_jobs_priority'
22
+ end
23
+
24
+ def self.down
25
+ drop_table :delayed_jobs
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateEventExceptions < (
4
+ Rails.version < '5.2' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
5
+ )
6
+ def change
7
+ create_table :event_exceptions do |t|
8
+ t.string :event_entity
9
+ t.string :event_action
10
+ t.text :event_data
11
+ t.string :listener_class
12
+ t.string :listener_instance
13
+ t.string :exception_class
14
+ t.string :exception_message
15
+ t.text :exception_stack
16
+
17
+ t.timestamps null: false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateListenerOptions < (
4
+ Rails.version < '5.2' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
5
+ )
6
+ def change
7
+ create_table :listener_options do |t|
8
+ t.string :listener_class
9
+ t.integer :delay
10
+
11
+ t.timestamps null: false
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddEnabledToListenerOptions < (
4
+ Rails.version < '5.2' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
5
+ )
6
+ def change
7
+ add_column :listener_options, :enabled, :boolean, null: false, default: true
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RenamePluginEventsManagerToRedmineEventsManager < (
4
+ Rails.version < '5.2' ? ActiveRecord::Migration : ActiveRecord::Migration[4.2]
5
+ )
6
+ class Settings < ActiveRecord::Base
7
+ end
8
+
9
+ def up
10
+ rename_plugin('events_manager', 'redmine_events_manager')
11
+ end
12
+
13
+ def down
14
+ rename_plugin('redmine_events_manager', 'events_manager')
15
+ end
16
+
17
+ private
18
+
19
+ def rename_plugin(from, to)
20
+ old_record = Setting.find_by(name: from)
21
+ return if old_record.blank?
22
+
23
+ new_record = Setting.find_by(name: to)
24
+ if new_record.present?
25
+ new_value = plugin_setting_value(old_record).value.merge(plugin_setting_value(new_record))
26
+ new_record.update!(value: new_value)
27
+ old_record.destroy!
28
+ else
29
+ old_record.update!(name: to)
30
+ end
31
+ end
32
+
33
+ def plugin_setting_value(record)
34
+ if record.value.blank?
35
+ {}.with_indifferent_access
36
+ elsif record.value.is_a?(Hash)
37
+ record.value
38
+ else
39
+ raise "#{record} value is not Hash or blank"
40
+ end
41
+ end
42
+ end
data/init.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redmine'
4
+
5
+ require 'events_manager/patches/issue_patch'
6
+ require 'events_manager/patches/issue_relation_patch'
7
+ require 'events_manager/patches/journal_patch'
8
+ require 'events_manager/patches/test_case_patch'
9
+ require 'events_manager/patches/time_entry_patch'
10
+ require 'events_manager/patches/repository/git_patch'
11
+ require 'events_manager'
12
+ require_dependency 'events_manager/hooks/add_assets'
13
+ require_dependency 'redmine_events_manager/version'
14
+
15
+ Redmine::Plugin.register :redmine_events_manager do
16
+ name 'Events Manager'
17
+ author ::RedmineEventsManager::AUTHOR
18
+ description ::RedmineEventsManager::SUMMARY
19
+ version ::RedmineEventsManager::VERSION
20
+
21
+ settings default: { event_exception_unchecked: false }
22
+
23
+ Redmine::MenuManager.map :admin_menu do |menu|
24
+ menu.push :event_exceptions, { controller: 'event_exceptions', action: 'index', id: nil },
25
+ caption: :label_event_exception_plural
26
+ menu.push :listener_options, { controller: 'listener_options', action: 'index', id: nil },
27
+ caption: :label_listener_option_plural
28
+ end
29
+
30
+ Redmine::MenuManager.map :top_menu do |menu|
31
+ menu.push :event_exception_unchecked,
32
+ { controller: 'event_exceptions', action: 'index', id: nil },
33
+ caption: '', last: true, if: proc {
34
+ User.current.admin? && EventsManager::Settings.event_exception_unchecked
35
+ }
36
+ end
37
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ class << self
5
+ attr_accessor :delay_disabled, :log_exceptions_disabled
6
+
7
+ EVENT_EXCEPTION_ATTRIBUTES = {
8
+ event_entity: proc { |e, _l, _ex| e.entity.name },
9
+ event_action: proc { |e, _l, _ex| e.action.to_s },
10
+ event_data: proc { |e, _l, _ex| e.data.to_yaml },
11
+ listener_class: proc { |_e, l, _ex| l.class.name },
12
+ listener_instance: proc { |_e, l, _ex| l.to_s },
13
+ exception_class: proc { |_e, _l, ex| ex.class.name },
14
+ exception_message: proc { |_e, _l, ex| ex.message },
15
+ exception_stack: proc { |_e, _l, ex| ex.backtrace.join("\n") }
16
+ }.freeze
17
+
18
+ def add_listener(entity, action, listener)
19
+ return if listeners(entity, action).include?(listener)
20
+
21
+ listeners(entity, action) << listener.to_s
22
+ end
23
+
24
+ def trigger(entity, action, data)
25
+ event = EventsManager::Event.new(entity, action, data)
26
+ Rails.logger.debug("Event triggered: #{event}")
27
+ listeners(entity, action).each do |l|
28
+ Rails.logger.debug("Listener found: #{l}")
29
+ run_delayed_listener(event, l.constantize.new(event))
30
+ end
31
+ end
32
+
33
+ def all_listeners
34
+ r = []
35
+ @listeners.each_value do |e|
36
+ e.each_value do |a|
37
+ r += a
38
+ end
39
+ end
40
+ r.uniq
41
+ end
42
+
43
+ private
44
+
45
+ def run_delayed_listener(event, listener)
46
+ return unless ::ListenerOption.listener_enabled?(listener.class)
47
+
48
+ if delay_disabled
49
+ run_listener(event, listener)
50
+ else
51
+ delay(run_at: ::ListenerOption.listener_delay(listener.class).seconds.from_now)
52
+ .run_listener(event, listener)
53
+ end
54
+ end
55
+
56
+ def run_listener(event, listener)
57
+ previous_locale = I18n.locale
58
+ begin
59
+ Rails.logger.info("Running listener: #{listener}")
60
+ I18n.locale = Setting.default_language
61
+ listener.run
62
+ rescue StandardError => e
63
+ on_listener_exception(event, listener, e)
64
+ ensure
65
+ I18n.locale = previous_locale
66
+ end
67
+ end
68
+
69
+ def on_listener_exception(event, listener, exception)
70
+ raise exception if log_exceptions_disabled
71
+
72
+ Rails.logger.warn(exception)
73
+ begin
74
+ EventsManager::Settings.event_exception_unchecked = true
75
+ EventException.create!(event_exception_data(event, listener, exception))
76
+ rescue StandardError => e
77
+ Rails.logger.warn(e)
78
+ end
79
+ end
80
+
81
+ def event_exception_data(event, listener, exception)
82
+ data = {}
83
+ EVENT_EXCEPTION_ATTRIBUTES.each do |a, p|
84
+ data[a] = begin
85
+ p.call(event, listener, exception)
86
+ rescue StandardError => e
87
+ e.to_s
88
+ end
89
+ end
90
+ data
91
+ end
92
+
93
+ def listeners(entity, action)
94
+ @listeners ||= {}
95
+ @listeners[entity] ||= {}
96
+ @listeners[entity][action] ||= []
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ class Event
5
+ attr_reader :entity, :action, :data
6
+
7
+ def initialize(entity, action, data)
8
+ @entity = entity
9
+ @action = action
10
+ @data = data
11
+ end
12
+
13
+ def to_s
14
+ "#{entity}::#{action}|#{data.class}(#{data.id})"
15
+ end
16
+
17
+ def issue_create?
18
+ entity == ::Issue && action == :create
19
+ end
20
+
21
+ def issue_update?
22
+ entity == ::Issue && action == :update
23
+ end
24
+
25
+ def issue_relation_create?
26
+ entity == ::IssueRelation && action == :create
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ module Hooks
5
+ class AddAssets < Redmine::Hook::ViewListener
6
+ def view_layouts_base_html_head(_context = {})
7
+ stylesheet_link_tag(:application, plugin: 'redmine_events_manager') + "\n"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ module Patches
5
+ module IssuePatch
6
+ def self.included(base)
7
+ base.send(:include, InstanceMethods)
8
+
9
+ base.class_eval do
10
+ unloadable
11
+
12
+ after_create :issue_create_event
13
+ after_destroy :issue_destroy_event
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def issue_create_event
19
+ EventsManager.trigger(Issue, :create, self)
20
+ end
21
+
22
+ def issue_destroy_event
23
+ EventsManager.trigger(Issue, :delete, self)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ unless Issue.included_modules.include? EventsManager::Patches::IssuePatch
31
+ Issue.include EventsManager::Patches::IssuePatch
32
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ module Patches
5
+ module IssueRelationPatch
6
+ def self.included(base)
7
+ base.send(:include, InstanceMethods)
8
+
9
+ base.class_eval do
10
+ unloadable
11
+
12
+ after_create :issue_relation_create_event
13
+ after_destroy :issue_relation_destroy_event
14
+ end
15
+ end
16
+
17
+ module InstanceMethods
18
+ def issue_relation_create_event
19
+ EventsManager.trigger(IssueRelation, :create, self)
20
+ end
21
+
22
+ def issue_relation_destroy_event
23
+ EventsManager.trigger(IssueRelation, :delete, self)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ unless IssueRelation.included_modules.include? EventsManager::Patches::IssueRelationPatch
31
+ IssueRelation.include EventsManager::Patches::IssueRelationPatch
32
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ module Patches
5
+ module JournalPatch
6
+ def self.included(base)
7
+ base.send(:include, InstanceMethods)
8
+
9
+ base.class_eval do
10
+ unloadable
11
+
12
+ after_create :journal_create_event
13
+ end
14
+ end
15
+
16
+ module InstanceMethods
17
+ def journal_create_event
18
+ return unless journalized_type == 'Issue'
19
+
20
+ EventsManager.trigger(Issue, :update, self)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ unless Journal.included_modules.include? EventsManager::Patches::JournalPatch
28
+ Journal.include EventsManager::Patches::JournalPatch
29
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ module Patches
5
+ module Repository
6
+ module GitPatch
7
+ def fetch_changesets
8
+ super
9
+ EventsManager.trigger(::Repository, :receive, self)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ unless ::Repository::Git.included_modules.include?(EventsManager::Patches::Repository::GitPatch)
17
+ ::Repository::Git.prepend(EventsManager::Patches::Repository::GitPatch)
18
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ module Patches
5
+ module TestCasePatch
6
+ def self.included(base)
7
+ base.setup { ::RedmineEventsManager::TestConfig.new.before_each }
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ if Rails.env.test?
14
+ unless ::ActiveSupport::TestCase.included_modules.include? EventsManager::Patches::TestCasePatch
15
+ ::ActiveSupport::TestCase.include EventsManager::Patches::TestCasePatch
16
+ end
17
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ module Patches
5
+ module TimeEntryPatch
6
+ def self.included(base)
7
+ base.send(:include, InstanceMethods)
8
+
9
+ base.class_eval do
10
+ unloadable
11
+
12
+ after_create :time_entry_create_event
13
+ after_destroy :time_entry_destroy_event
14
+ after_update :time_entry_update_event
15
+ end
16
+ end
17
+
18
+ module InstanceMethods
19
+ def time_entry_create_event
20
+ EventsManager.trigger(TimeEntry, :create, self)
21
+ end
22
+
23
+ def time_entry_destroy_event
24
+ EventsManager.trigger(TimeEntry, :delete,
25
+ EventsManager::RemovedRecord.new(self))
26
+ end
27
+
28
+ def time_entry_update_event
29
+ EventsManager.trigger(TimeEntry, :update,
30
+ EventsManager::UpdatedRecord.new(self))
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ unless TimeEntry.included_modules.include? EventsManager::Patches::TimeEntryPatch
38
+ TimeEntry.include EventsManager::Patches::TimeEntryPatch
39
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ class RemovedRecord
5
+ def initialize(record)
6
+ @attributes = record.attributes
7
+ @class = record.class
8
+ end
9
+
10
+ def copy_record
11
+ r = @class.new
12
+ @attributes.each do |k, v|
13
+ r.send("#{k}=", v)
14
+ end
15
+ r
16
+ end
17
+
18
+ def id
19
+ @attributes[:id]
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ class Settings
5
+ class << self
6
+ def event_exception_unchecked=(value)
7
+ s = Setting.plugin_redmine_events_manager.dup || {}
8
+ s[:event_exception_unchecked] = value ? true : false
9
+ Setting.plugin_redmine_events_manager = s
10
+ end
11
+
12
+ def event_exception_unchecked
13
+ Setting.plugin_redmine_events_manager[:event_exception_unchecked] ? true : false
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventsManager
4
+ class UpdatedRecord
5
+ attr_reader :record, :changes
6
+
7
+ def initialize(record)
8
+ @record = record
9
+ @changes = record.changes
10
+ end
11
+
12
+ delegate :id, to: :record
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedmineEventsManager
4
+ class TestConfig
5
+ def before_each
6
+ EventsManager.delay_disabled = true
7
+ EventsManager.log_exceptions_disabled = true
8
+ EventsManager::Settings.event_exception_unchecked = false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedmineEventsManager
4
+ AUTHOR = 'Eduardo Henrique Bogoni'
5
+ HOMEPAGE = 'https://github.com/esquilo-azul/redmine_events_manager'
6
+ SUMMARY = 'Management for events'
7
+ VERSION = '0.4.8'
8
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :events_manager do
4
+ namespace :events do
5
+ namespace :issue_relation do
6
+ desc 'Envia notificações da criação de um IssueRelation'
7
+ task :create, [:issue_relation_id] => :environment do |_t, args|
8
+ EventsManager.trigger(
9
+ IssueRelation,
10
+ :create,
11
+ IssueRelation.find(args.issue_relation_id)
12
+ )
13
+ end
14
+ end
15
+ namespace :issue do
16
+ desc 'Envia notificações da criação de um Issue'
17
+ task :create, [:issue_id] => :environment do |_t, args|
18
+ EventsManager.trigger(
19
+ Issue,
20
+ :create,
21
+ Issue.find(args.issue_id)
22
+ )
23
+ end
24
+
25
+ desc 'Envia notificações da alteração de um Issue'
26
+ task :update, [:journal_id] => :environment do |_t, args|
27
+ EventsManager.trigger(
28
+ Issue,
29
+ :update,
30
+ Journal.find(args.journal_id)
31
+ )
32
+ end
33
+ end
34
+ namespace :repository do
35
+ desc 'Ativa evento de recebimento de conteúdo por repositório'
36
+ task :receive, [:repository_id] => :environment do |_t, args|
37
+ EventsManager.delay_disabled = true
38
+ EventsManager.trigger(
39
+ Repository,
40
+ :receive,
41
+ Repository.find(args.repository_id)
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redmine_plugins_helper/plugin_rake_task'
4
+ ::RedminePluginsHelper::PluginRakeTask.register(:redmine_events_manager, :test)
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path('lib', __dir__)
4
+
5
+ # Maintain your gem's version:
6
+ require 'redmine_events_manager/version'
7
+
8
+ # Describe your gem and declare its dependencies:
9
+ Gem::Specification.new do |s|
10
+ s.name = 'redmine_events_manager'
11
+ s.version = ::RedmineEventsManager::VERSION
12
+ s.authors = [::RedmineEventsManager::AUTHOR]
13
+ s.summary = ::RedmineEventsManager::SUMMARY
14
+ s.homepage = ::RedmineEventsManager::HOMEPAGE
15
+
16
+ s.require_paths = ['lib']
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files spec test`.split("\n")
19
+
20
+ s.add_dependency 'delayed_job_active_record'
21
+
22
+ # Test/development gems
23
+ s.add_development_dependency 'eac_ruby_gem_support', '~> 0.1', '>= 0.1.2'
24
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe EventExceptionsController, type: :feature do
4
+ include_context 'with logged user', 'admin' do
5
+ before do
6
+ EventsManager::Settings.event_exception_unchecked = true
7
+ end
8
+
9
+ it { expect(EventsManager::Settings.event_exception_unchecked).to be_truthy }
10
+ it { expect(::User.current.login).to eq('admin') }
11
+
12
+ context 'when index is accessed' do
13
+ before { visit '/event_exceptions' }
14
+
15
+ it { expect(EventsManager::Settings.event_exception_unchecked).to be_falsy }
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe(::EventsManager) do
4
+ let(:stub_entity_class) { ::Class.new }
5
+ let(:stub_listener_class) { ::Class.new }
6
+
7
+ before do
8
+ described_class.add_listener(stub_entity_class, 'create', stub_listener_class)
9
+ described_class.add_listener(stub_entity_class, 'update', stub_listener_class)
10
+ end
11
+
12
+ it { expect(described_class.all_listeners).to be_a(Array) }
13
+ it { expect(described_class.all_listeners.count).to be_positive }
14
+ it { expect(described_class.all_listeners).to(be_all { |l| l.is_a?(::String) }) }
15
+ it { expect(described_class.all_listeners.count).to eq(described_class.all_listeners.uniq.count) }
16
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ class DummyListener
4
+ def initialize(event)
5
+ @event = event
6
+ end
7
+
8
+ def to_s
9
+ 'dummy_listener_instance'
10
+ end
11
+
12
+ def run
13
+ raise 'Dummy failed!' if @event.data.failed
14
+ end
15
+ end
16
+
17
+ class DummyEntity
18
+ attr_reader :failed
19
+
20
+ def initialize(failed)
21
+ @failed = failed
22
+ end
23
+
24
+ def to_s
25
+ "dummy_entity_instance_#{failed}"
26
+ end
27
+
28
+ def id
29
+ 1
30
+ end
31
+ end
32
+
33
+ RSpec.describe EventException do
34
+ let(:event_exception_count_start) { described_class.count }
35
+
36
+ before do
37
+ EventsManager.log_exceptions_disabled = false
38
+ event_exception_count_start
39
+ EventsManager.add_listener(DummyEntity, :create, 'DummyListener')
40
+ end
41
+
42
+ context 'when event triggered is successful' do
43
+ before do
44
+ EventsManager.trigger(DummyEntity, :create, DummyEntity.new(false))
45
+ end
46
+
47
+ it 'successful event should not generate event exception' do
48
+ expect(described_class.count).to eq(event_exception_count_start)
49
+ end
50
+
51
+ it { expect(EventsManager::Settings.event_exception_unchecked).to be_falsy }
52
+ end
53
+
54
+ context 'when event triggered is fail' do
55
+ let(:ee) { described_class.last }
56
+
57
+ before do
58
+ EventsManager.trigger(DummyEntity, :create, DummyEntity.new(true))
59
+ end
60
+
61
+ it 'failed event should generate event exception' do
62
+ expect(described_class.count).to eq(event_exception_count_start + 1)
63
+ end
64
+
65
+ it { expect(ee).to be_truthy }
66
+ it { expect(ee.event_entity).to eq('DummyEntity') }
67
+ it { expect(ee.event_action).to eq('create') }
68
+ it { expect(ee.event_data).to eq(<<~DATA_CONTENT) }
69
+ --- !ruby/object:DummyEntity
70
+ failed: true
71
+ DATA_CONTENT
72
+ it { expect(ee.listener_class).to eq('DummyListener') }
73
+ it { expect(ee.listener_instance).to eq('dummy_listener_instance') }
74
+ it { expect(ee.exception_class).to eq('RuntimeError') }
75
+ it { expect(ee.exception_message).to eq('Dummy failed!') }
76
+ it { expect(ee.exception_stack).to be_present }
77
+ it { expect(EventsManager::Settings.event_exception_unchecked).to be_truthy }
78
+ end
79
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redmine_events_manager
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.8
5
+ platform: ruby
6
+ authors:
7
+ - Eduardo Henrique Bogoni
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: delayed_job_active_record
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: eac_ruby_gem_support
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.1'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 0.1.2
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '0.1'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 0.1.2
47
+ description:
48
+ email:
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - ".avm.yml"
54
+ - ".gitignore"
55
+ - ".rubocop.yml"
56
+ - SelfGemfile
57
+ - app/controllers/event_exceptions_controller.rb
58
+ - app/controllers/listener_options_controller.rb
59
+ - app/models/event_exception.rb
60
+ - app/models/listener_option.rb
61
+ - assets/stylesheets/application.css
62
+ - bin/bundle
63
+ - config/initializers/000_dependencies.rb
64
+ - config/initializers/assets.rb
65
+ - config/locales/pt-BR.yml
66
+ - config/routes.rb
67
+ - db/migrate/20160509152749_create_delayed_jobs.rb
68
+ - db/migrate/20160717222418_create_event_exceptions.rb
69
+ - db/migrate/20180331122033_create_listener_options.rb
70
+ - db/migrate/20180419202618_add_enabled_to_listener_options.rb
71
+ - db/migrate/20180711170315_rename_plugin_events_manager_to_redmine_events_manager.rb
72
+ - init.rb
73
+ - lib/events_manager.rb
74
+ - lib/events_manager/event.rb
75
+ - lib/events_manager/hooks/add_assets.rb
76
+ - lib/events_manager/patches/issue_patch.rb
77
+ - lib/events_manager/patches/issue_relation_patch.rb
78
+ - lib/events_manager/patches/journal_patch.rb
79
+ - lib/events_manager/patches/repository/git_patch.rb
80
+ - lib/events_manager/patches/test_case_patch.rb
81
+ - lib/events_manager/patches/time_entry_patch.rb
82
+ - lib/events_manager/removed_record.rb
83
+ - lib/events_manager/settings.rb
84
+ - lib/events_manager/updated_record.rb
85
+ - lib/redmine_events_manager/test_config.rb
86
+ - lib/redmine_events_manager/version.rb
87
+ - lib/tasks/events.rake
88
+ - lib/tasks/redmine_events_manager.rake
89
+ - redmine_events_manager.gemspec
90
+ - spec/controllers/event_exceptions_controller_spec.rb
91
+ - spec/lib/events_manager_spec.rb
92
+ - spec/models/event_exception_spec.rb
93
+ homepage: https://github.com/esquilo-azul/redmine_events_manager
94
+ licenses: []
95
+ metadata: {}
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.0.8
112
+ signing_key:
113
+ specification_version: 4
114
+ summary: Management for events
115
+ test_files:
116
+ - spec/controllers/event_exceptions_controller_spec.rb
117
+ - spec/lib/events_manager_spec.rb
118
+ - spec/models/event_exception_spec.rb