icentris-rules 0.9

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 (57) hide show
  1. checksums.yaml +15 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +33 -0
  5. data/app/assets/images/pyr_rules/ui-anim_basic_16x16.gif +0 -0
  6. data/app/assets/javascripts/pyr_rules/rules.js +135 -0
  7. data/app/assets/stylesheets/pyr_rules/rules.css.scss +258 -0
  8. data/app/assets/stylesheets/scaffold.css +57 -0
  9. data/app/controllers/pyr_rules/actions_controller.rb +22 -0
  10. data/app/controllers/pyr_rules/events_controller.rb +17 -0
  11. data/app/controllers/pyr_rules/rules_controller.rb +233 -0
  12. data/app/helpers/pyr_rules/actions_helper.rb +2 -0
  13. data/app/helpers/pyr_rules/events_helper.rb +95 -0
  14. data/app/helpers/pyr_rules/rules_helper.rb +10 -0
  15. data/app/models/pyr_rules.rb +3 -0
  16. data/app/models/pyr_rules/action.rb +116 -0
  17. data/app/models/pyr_rules/rule.rb +201 -0
  18. data/app/rules/pyr_rules/action_handler.rb +94 -0
  19. data/app/rules/pyr_rules/controller_event_emitter.rb +34 -0
  20. data/app/rules/pyr_rules/email_action_handler.rb +6 -0
  21. data/app/rules/pyr_rules/event_concerns.rb +23 -0
  22. data/app/rules/pyr_rules/event_config_loader.rb +97 -0
  23. data/app/rules/pyr_rules/event_subscriber.rb +36 -0
  24. data/app/rules/pyr_rules/model_creation_handler.rb +25 -0
  25. data/app/rules/pyr_rules/model_event_emitter.rb +80 -0
  26. data/app/rules/pyr_rules/rule_context.rb +20 -0
  27. data/app/rules/pyr_rules/rules_config.rb +87 -0
  28. data/app/rules/pyr_rules/rules_engine.rb +38 -0
  29. data/app/rules/pyr_rules/web_request_logger.rb +6 -0
  30. data/app/views/pyr_rules/actions/index.html.erb +12 -0
  31. data/app/views/pyr_rules/events/_sidebar.html.erb +8 -0
  32. data/app/views/pyr_rules/events/index.html.erb +72 -0
  33. data/app/views/pyr_rules/events/show.html.erb +11 -0
  34. data/app/views/pyr_rules/rules/_form.html.erb +28 -0
  35. data/app/views/pyr_rules/rules/_ready_headers.js.erb +11 -0
  36. data/app/views/pyr_rules/rules/_rule_context_field.html.erb +10 -0
  37. data/app/views/pyr_rules/rules/_rules.html.erb +34 -0
  38. data/app/views/pyr_rules/rules/add_action_mapping.js.erb +13 -0
  39. data/app/views/pyr_rules/rules/edit.html.erb +14 -0
  40. data/app/views/pyr_rules/rules/index.html.erb +5 -0
  41. data/app/views/pyr_rules/rules/lookup_sub_properties.js.erb +13 -0
  42. data/app/views/pyr_rules/rules/new.html.erb +11 -0
  43. data/app/views/pyr_rules/rules/remove_action_mapping.js.erb +15 -0
  44. data/app/views/pyr_rules/rules/show.html.erb +98 -0
  45. data/app/views/shared/_action.html.erb +41 -0
  46. data/app/views/shared/_action_field_mapping.html.erb +33 -0
  47. data/app/views/shared/_event.html.erb +21 -0
  48. data/app/views/shared/_rule.html.erb +27 -0
  49. data/config/routes.rb +32 -0
  50. data/config/security.yml +9 -0
  51. data/db/seeds.rb +1 -0
  52. data/lib/pyr_rules.rb +34 -0
  53. data/lib/pyr_rules/engine.rb +72 -0
  54. data/lib/pyr_rules/generators/dummy_generator.rb +27 -0
  55. data/lib/pyr_rules/version.rb +3 -0
  56. data/lib/tasks/pyr_rules_tasks.rake +57 -0
  57. metadata +224 -0
@@ -0,0 +1,116 @@
1
+ class PyrRules::Action
2
+ include Mongoid::Document
3
+
4
+ @@context_mapping_equivalencies ||= {} # {:messaging_user => [:user, :tree_user, :contact] }
5
+
6
+ field :title, type: String
7
+ field :type, type: String # class name of handler
8
+ field :context_mapping, type: Hash, default: {} # Map of actionNeed => ruleContextField {"actor:user"=>"actor:user"}
9
+ field :template, type: Hash, default: {} # Map of templateName => templateValue { "email" => "hello {name}" }
10
+
11
+ embedded_in :rule, inverse_of: :actions
12
+
13
+ before_save :set_default_mappings
14
+
15
+ # An action is ready if all of it's needs are mapped
16
+ def ready?
17
+ needs.each do |field, type|
18
+ return false unless lookup_context_mapping(field,type)
19
+ end
20
+ true
21
+ end
22
+
23
+ def needs
24
+ n = (type.constantize.configuration[:needs].dup rescue {}) || {}
25
+ class_lookup_cols = {}
26
+ n.each do |field,type|
27
+ if type == :class_lookup
28
+ klazz = self.lookup_context_mapping(field,:class_lookup)[0] rescue nil
29
+ class_lookup_cols.merge! PyrRules::Rule.get_columns(klazz.camelcase.constantize) if klazz
30
+ end
31
+ end
32
+ class_lookup_cols.each do |column, col_type|
33
+ n[column] = col_type
34
+ end
35
+ template_names.each do |name|
36
+ template_fields(name).each do |f|
37
+ n[f.to_sym] = :string
38
+ end
39
+ end
40
+ n
41
+ end
42
+
43
+ # What templates are defined for this Action's ActionHandler?
44
+ # This reads from template DSL inside of ActionHandler class
45
+ # Templates have to be defined through the DSL!!!
46
+ def template_configuration
47
+ (type.constantize.configuration[:templates] rescue {}) || {}
48
+ end
49
+
50
+
51
+ def template_names
52
+ template_configuration.keys rescue []
53
+ end
54
+
55
+ def template_body(template_name)
56
+ body = template[template_name.to_s] # First try to read from Mongo Document template Hash
57
+ body ||= template_configuration[template_name.to_sym] # Not found, use the default configuration from the ActionHandler DSL
58
+ end
59
+
60
+ # Extract the template fields for the provided template
61
+ #
62
+ # eg: body = "Hello {name}" - the template field is :name
63
+ #
64
+ def template_fields(template_name)
65
+ body = template_body(template_name)
66
+ body.scan(/\{([^\s]*)\}/).flatten rescue []
67
+ end
68
+
69
+ # returns an array [field,type]
70
+ def lookup_context_mapping(field, type = "string")
71
+ context_mapping["#{field}:#{type}"].split(":") rescue nil
72
+ end
73
+
74
+ def set_default_mappings(rule_context = rule.context)
75
+ needs.each do |action_field, type|
76
+ puts "...Considering mapping for #{action_field}(#{type})"
77
+ rule_context.keys.each do |rule_field|
78
+ unless try_rule_mapping(rule_context,rule_field,rule_field,action_field,type)
79
+ try_rule_mapping(rule_context,rule_field,rule_field.split(".").last.to_sym,action_field,type)
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ # A mapping equivalency is used to indicate two types can be mapped to each other, even though
86
+ # they do not have the same exact name
87
+ def self.add_mapping_type_equivalency(target_type, equivalent_types)
88
+ @@context_mapping_equivalencies[target_type] = equivalent_types
89
+ end
90
+
91
+ def self.check_mapping_type?(action_type, rule_type)
92
+ return true if action_type == rule_type || action_type == "string"
93
+ equivalencies = @@context_mapping_equivalencies[action_type.to_sym]
94
+ if equivalencies && equivalencies.index(rule_type.to_sym)
95
+ return true
96
+ end
97
+ false
98
+ end
99
+
100
+
101
+ private
102
+ def try_rule_mapping(rule_context,rule_field,rule_field_check,action_field,type)
103
+ if rule_field_check == action_field
104
+ rule_type = rule_context[rule_field]
105
+ if rule_type && rule_type.to_sym == type.to_sym
106
+ # The name and type matched - make the default entry
107
+ puts "...Adding default mapping for #{action_field}(#{type} ==> #{rule_field}:#{type}"
108
+ context_mapping["#{action_field}:#{type}"] = "#{rule_field}:#{type}"
109
+ return true
110
+ end
111
+ return false
112
+ end
113
+ end
114
+
115
+
116
+ end
@@ -0,0 +1,201 @@
1
+ require 'pyr_rules/action'
2
+
3
+
4
+ class PyrRules::Rule
5
+ include Mongoid::Document
6
+ include Mongoid::Timestamps::Short # For c_at and u_at.
7
+ include Mongoid::Versioning
8
+ include PyrRules::ModelEventEmitter
9
+
10
+ before_save :set_updated_by
11
+
12
+ max_versions 25
13
+
14
+ field :name, type: String
15
+ field :description, type: String
16
+ field :events, type: Array, default: []
17
+ field :criteria, type: String
18
+ field :active, type: Boolean
19
+ field :updated_by, type: String
20
+ field :start_date, type: Time
21
+ field :end_date, type: Time
22
+
23
+ # These can programatically be used to populate the events
24
+ field :event_inclusion_matcher, type: Regexp
25
+ field :event_exclusion_matcher, type: Regexp
26
+
27
+ embeds_many :actions, inverse_of: :rule #cascade_callbacks: true
28
+
29
+ accepts_nested_attributes_for :actions
30
+
31
+ validates :name, presence: true
32
+
33
+ # validates_each :events do |record, attr, event_list|
34
+ # if event_list
35
+ # event_list.each do |e|
36
+ # record.errors.add(attr, 'should be a valid event') unless PyrRules::RulesConfig.event e
37
+ # end
38
+ # end
39
+ # end
40
+
41
+ def ready?
42
+ return false unless actions
43
+ ! actions.collect{|a| a.ready?}.index false rescue false
44
+ end
45
+
46
+ def process_rule(event)
47
+ return unless check_criteria event
48
+ actions.each do |action|
49
+ begin
50
+ action_handler = action.type
51
+ klass = action_handler if action_handler.is_a? Class
52
+ #Module.const_get(action_handler) doesn't work for nested classes
53
+ #We can use String.constantize (ActiveSupport extension for string)
54
+ #or eval works for sure.
55
+ klass = action_handler.constantize rescue nil if action_handler.is_a? String
56
+ klass.new(event, action).handle if klass
57
+ rescue Exception => e
58
+ puts "\nERROR: Running action #{action.type} for rule #{name}: #{e.message}\n"
59
+ puts e.backtrace
60
+ end
61
+ end
62
+ end
63
+
64
+
65
+ # Determine overall Rule-scoped context via intersection all event-scoped contexts
66
+ def context
67
+ rc = {}
68
+ return rc if events == nil || events.size == 0
69
+ events.each_with_index do |e,i|
70
+ ec = PyrRules::RulesConfig.event_config(e)[:context] rescue {}
71
+ if i == 0
72
+ rc.merge!(ec) # start with the first event's context
73
+ else
74
+ rc.each do |field, metadata|
75
+ if ec[field] == nil
76
+ rc.delete field # this event did not have the field, it cannot be in our interesected context
77
+ elsif ec[field] != rc[field]
78
+ rc.delete field # this event had the field, but it was a different type, it cannot be in our intersected context
79
+ end
80
+ end
81
+ end
82
+ end
83
+ HashWithIndifferentAccess.new(rc)
84
+ end
85
+
86
+ def context_field_mapped?(field,type)
87
+ lookup = "#{field}:#{type}"
88
+ actions.each do |a|
89
+ return true if a.context_mapping.values.index lookup rescue false
90
+ end
91
+ false
92
+ end
93
+
94
+
95
+ def set_default_mappings
96
+ puts "Setting default action mappings"
97
+ rule_context = context # {:actor=>:user, :target=>:object, etc...}
98
+ actions.each do |a|
99
+ a.set_default_mappings(rule_context)
100
+ end
101
+ end
102
+
103
+ def self.rule_context(rule_event, extra_fields = {})
104
+ PyrRules::RuleContext.new rule_event, extra_fields
105
+ end
106
+
107
+ def check_criteria(rule_event)
108
+ return false unless events && events.index("#{rule_event[:klazz]}::#{rule_event[:action]}")
109
+ return true if criteria.blank?
110
+ result = eval criteria, PyrRules::Rule.rule_context(rule_event).get_binding
111
+ result
112
+ end
113
+
114
+ # called like : lookup_path("target")
115
+ # or : lookup_path("target.user")
116
+ def lookup_path(path, sub_context = nil)
117
+ return context if (path.blank? || path == "/") && !sub_context # Default for retrieving root context path
118
+ # path = "target.user" parts[0]="target" parts[1]="user"
119
+ parts = path.split(".")
120
+ if sub_context.nil?
121
+ if parts.first == "actor"
122
+ parts.shift
123
+ path=parts.join(".")
124
+ klazz = User
125
+ return lookup_path(path, User) unless path.blank?
126
+ elsif parts.first == "target"
127
+ parts.shift
128
+ path=parts.join(".")
129
+ cols = nil
130
+ # We need to get all the possible path values from all the input events
131
+ events.each do |event|
132
+ klazz = (event.split("::")[0...-1].join("::")).constantize
133
+ puts klazz
134
+ sub_cols = lookup_path(path, klazz)
135
+ puts sub_cols
136
+ cols ||= sub_cols
137
+ # This will perform an intersection across all the event fields
138
+ cols.each do |k,v|
139
+ unless sub_cols[k]==v
140
+ puts "Removing due to intersection #{k}"
141
+ cols.delete k
142
+ end
143
+ end
144
+ end
145
+ puts "Returning"
146
+ puts cols
147
+ return cols
148
+ end
149
+ end
150
+ klazz ||= sub_context || self
151
+
152
+ cols = {}
153
+ if parts.length == 0
154
+ cols = get_columns(klazz)
155
+ else
156
+ puts "DBH: What is this use case? I can't remember why I coded it?"
157
+ if klazz.respond_to? :reflect_on_all_associations
158
+ (klazz.reflect_on_all_associations(:has_one) +
159
+ klazz.reflect_on_all_associations(:belongs_to)).each do |assoc|
160
+ if (assoc.name.to_s == parts.first)
161
+ parts.shift
162
+ path=parts.join(".")
163
+ klazz = assoc.klass
164
+ return lookup_path(path, klazz)
165
+ end
166
+ end
167
+ end
168
+ end
169
+ cols
170
+ end
171
+
172
+ def self.get_columns(klazz)
173
+ cols = {}
174
+ if klazz.respond_to? :reflect_on_all_associations
175
+ (klazz.reflect_on_all_associations(:has_one) +
176
+ klazz.reflect_on_all_associations(:belongs_to)).each do |assoc|
177
+ cols[assoc.name] = assoc.name.to_sym
178
+ end
179
+ end
180
+ if klazz.respond_to? :columns # ActiveRecord
181
+ klazz.columns.each{|c| cols[c.name] = c.type.to_sym }
182
+ end
183
+ if klazz.respond_to? :fields # Mongoid
184
+ fields.each{|k,v| cols[v.name] = v.options[:type].name.downcase.to_sym }
185
+ end
186
+ cols
187
+ end
188
+
189
+ private
190
+ def set_updated_by
191
+ if Thread.current[:user]
192
+ self.updated_by = Thread.current[:user].try(:username)
193
+ end
194
+ end
195
+ end
196
+
197
+
198
+
199
+
200
+
201
+
@@ -0,0 +1,94 @@
1
+ class PyrRules::ActionHandler
2
+ #
3
+ # needs is a map of required fields for each and every type of ActionHandler
4
+ #
5
+ @@needs ||= {}
6
+ @@action_configuration ||= {}
7
+
8
+ @@templates ||= {}
9
+
10
+ def self.needs(name, type=:string)
11
+ @@needs[self] ||= {}
12
+ @@needs[self][name] = type
13
+ end
14
+
15
+ # def self.needs_map
16
+ # @@needs[self]
17
+ # end
18
+
19
+ def self.template(name)
20
+ @@templates[self] ||= {}
21
+ @@templates[self][name] = (block_given?) ? yield : ""
22
+ end
23
+
24
+ def self.reload_configuration
25
+ puts "Reloading ActionHandler configuration"
26
+ @@action_configuration = {}
27
+ configuration
28
+ @@action_configuration.keys
29
+ end
30
+
31
+ # def self.templates
32
+ # @@templates[self]
33
+ # end
34
+ def self.configuration
35
+ return @@action_configuration[self] unless @@action_configuration.empty?
36
+ #if Rails.env.development?
37
+ Dir["#{Rails.root}/../**/*/app/**/action_handler/*.rb"].each do |p|
38
+ puts "require_dependency #{p}"
39
+ require_dependency p
40
+ end
41
+ #end
42
+ _add_needs_and_recurse(PyrRules::ActionHandler)
43
+ @@action_configuration[self]
44
+ end
45
+
46
+ def self.action_listing
47
+ configuration # force load
48
+ @@action_configuration.keys
49
+ end
50
+
51
+ def self._add_needs_and_recurse(klazz)
52
+ klazz.subclasses.each do |c|
53
+ @@action_configuration[c] ||= {}
54
+ @@action_configuration[c][:needs] = @@needs[c]
55
+ @@action_configuration[c][:templates] = @@templates[c]
56
+ _add_needs_and_recurse(c)
57
+ end
58
+ end
59
+
60
+ # Our context methods will be accessible through here
61
+ def method_missing(meth, *args, &block)
62
+ mapping_type = @action.needs[meth]
63
+ if mapping_type
64
+ rule_field = @action.lookup_context_mapping(meth, mapping_type)[0]
65
+ value = eval rule_field, @context.get_binding rescue nil
66
+ return value
67
+ end
68
+ super
69
+ end
70
+
71
+
72
+ attr_accessor :event, :action
73
+ def initialize(event, action)
74
+ @event = event
75
+ @action = action # Allow use of symbols or strings as keys target = event_hash[:klazz].constantize.find event_hash[:id].to_i rescue nil
76
+ @context = PyrRules::Rule.rule_context(@event)
77
+ end
78
+
79
+ def handle
80
+ puts "\n\n------- #{self.class.name} ------ \n\nHANDLE called for #{@event.inspect} \n\n---- #{@action.inspect}\n\n"
81
+ end
82
+
83
+ def eval_template(template_name)
84
+ template_name = template_name.to_s
85
+ puts "DEBUG: Looking up #{template_name} from #{@action.template}"
86
+ t = @action.template_body(template_name)
87
+ interpolated_fields = @action.template_fields(template_name)
88
+ interpolated_fields.each do |f|
89
+ t.gsub!("{#{f}}","\#{#{f}}")
90
+ end
91
+ puts "DEBUG: Using #{t}"
92
+ eval '"' + t + '"'
93
+ end
94
+ end
@@ -0,0 +1,34 @@
1
+ module PyrRules
2
+ module ControllerEventEmitter
3
+
4
+ @@registered_classes = []
5
+
6
+ def self.included(base)
7
+ #base.send :extend, ClassMethods
8
+ #unless ( File.basename($0) == "rake" && ARGV.include?("db:migrate") )
9
+ #end
10
+ base.around_filter :raise_controller_event
11
+ @@registered_classes << base if !@@registered_classes.index(base)
12
+ end
13
+
14
+ def self.registered_classes
15
+ @@registered_classes
16
+ end
17
+
18
+ private
19
+ def raise_controller_event(extras = {})
20
+ action = params[:action]
21
+ payload = build_event_payload("ControllerEvent", self.class.name, action, extras)
22
+ ActiveSupport::Notifications.instrument("PyrRules_#{self.class.name}_#{action}", payload ) do
23
+ yield
24
+ end
25
+ end
26
+
27
+ def build_event_payload(event_type, class_name, action, extras={})
28
+ payload = { type: event_type, klazz: self.class.name, action: action, data: params,
29
+ user: Thread.current[:user] }.merge(extras)
30
+ end
31
+
32
+
33
+ end
34
+ end
@@ -0,0 +1,6 @@
1
+ class PyrRules::EmailActionHandler < PyrRules::ActionHandler
2
+ def handle
3
+ super
4
+ puts "This is in Pyr-Rules. It echos template ------- #{@action.template}"
5
+ end
6
+ end