pigeons 0.0.1pre
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +676 -0
- data/README.md +29 -0
- data/Rakefile +10 -0
- data/lib/generators/pigeons/install_generator.rb +42 -0
- data/lib/generators/pigeons/templates/pigeon_letter.rb +30 -0
- data/lib/generators/pigeons/templates/pigeon_letter_migration.rb +16 -0
- data/lib/generators/pigeons/templates/pigeon_mailer.rb +8 -0
- data/lib/generators/pigeons/templates/pigeons.json +5 -0
- data/lib/pigeons/checks.rb +98 -0
- data/lib/pigeons/elements.rb +35 -0
- data/lib/pigeons/errors.rb +8 -0
- data/lib/pigeons/extensions.rb +34 -0
- data/lib/pigeons/logger.rb +15 -0
- data/lib/pigeons/pigeons.rb +156 -0
- data/lib/pigeons/pigeons_tasks.rb +8 -0
- data/lib/pigeons/scope.rb +205 -0
- data/lib/pigeons/version.rb +3 -0
- data/lib/pigeons.rb +10 -0
- data/pigeons.gemspec +29 -0
- data/tasks/pigeons.rake +142 -0
- data/test/test_helper.rb +143 -0
- data/test/test_pigeons.rb +429 -0
- metadata +200 -0
@@ -0,0 +1,205 @@
|
|
1
|
+
module Pigeons
|
2
|
+
|
3
|
+
module Scope
|
4
|
+
|
5
|
+
def self.get_scope(args)
|
6
|
+
elements = args[:elements]
|
7
|
+
previous_base_scope = args[:previous_base_scope]
|
8
|
+
previous_letters = args[:previous_letters]
|
9
|
+
flights = args[:flights]
|
10
|
+
flight = args[:flight]
|
11
|
+
letter_info = args[:letter_info]
|
12
|
+
flight_id = args[:flight_id]
|
13
|
+
opts = args[:opts]
|
14
|
+
|
15
|
+
Pigeons::Logger.debug [ 'Pigeons::', 'Scope::GetScope', 'Elements', elements ] if opts[:debug]
|
16
|
+
|
17
|
+
# We accept a running base_element
|
18
|
+
# -- gets
|
19
|
+
# then gets
|
20
|
+
# then gets
|
21
|
+
|
22
|
+
base_scope = previous_base_scope unless previous_base_scope.nil?
|
23
|
+
|
24
|
+
if elements[:base_element] # E.g. "users..."
|
25
|
+
base_scope = get_base_scope(elements[:base_element])
|
26
|
+
previous_letters = [] unless elements[:joiner] # Unless we have a joiner, we're ignoring previous letters
|
27
|
+
elsif elements[:joiner].nil? # "then..."
|
28
|
+
# must start with a base element or a joiner
|
29
|
+
raise PigeonError::PigeonFlightConfigError, "Missing a joiner in #{flight}, start a line with an elements (e.g. user) or and/then: #{letter_info}"
|
30
|
+
end
|
31
|
+
|
32
|
+
Pigeons::Logger.debug [ 'Pigeons::', 'Scope::GetScope', 'Base Element Count', "#{base_scope.name.number(base_scope.count)}" ] if opts[:debug]
|
33
|
+
|
34
|
+
raise PigeonError::PigeonFlightConfigError, "First flight element in #{flight} must specify base (e.g. user), but didn't: #{letter_info}" if base_scope.nil?
|
35
|
+
|
36
|
+
scope = base_scope.scoped # We'll start with a fresh
|
37
|
+
source_arel = scope.arel_table # This will be used for checking PigeonLetters.. or posts
|
38
|
+
pigeon_arel = PigeonLetter.arel_table # used in queries below
|
39
|
+
|
40
|
+
# First, we'll check by flight
|
41
|
+
# Flights are essentially cohorts
|
42
|
+
# All entites belong to one based on entity_id%x=y where x is the number of flights
|
43
|
+
# and y is the order of this flight in alphabetically order starting with 0
|
44
|
+
if flights.count > 0
|
45
|
+
scope = scope.where(["id%?=?",flights.count,flight_id])
|
46
|
+
end
|
47
|
+
|
48
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} must specify letter: #{letter_info}" if elements[:letter].nil?
|
49
|
+
|
50
|
+
# Convert the letter to snakecase
|
51
|
+
letter = elements[:letter].downcase.gsub(' ', '_') # "a ... letter"
|
52
|
+
|
53
|
+
# Make sure this letter is valid
|
54
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} referenced letter \"#{letter}\" that doesn't exist in PigeonMailer. Try adding \"def #{letter}(#{source_arel.name.singularize})\"" unless PigeonMailer.action_methods.include?(letter.to_s) || PigeonMailer.respond_to?(letter.to_s) # We'll take action_methods or respond_to?
|
55
|
+
|
56
|
+
# Next, we need to do the "based on letters" queries
|
57
|
+
|
58
|
+
# Todo/Question: what happens when a user switches cohorts and now deserves two item types
|
59
|
+
# we need to be mighty cautious here to preclude that
|
60
|
+
# Also, may happen with milestones
|
61
|
+
# TODO: This above, last big thing besides getting these queries right
|
62
|
+
# and renaming maybe from letters to posts, or not
|
63
|
+
# also, what are we actually sending
|
64
|
+
|
65
|
+
# First, we'll base it on cooldown
|
66
|
+
# Since we don't ever want to send an entity two letters on the same day
|
67
|
+
scope = Checks.no_recent_letter_check(scope: scope, source: source_arel, pigeon: pigeon_arel)
|
68
|
+
|
69
|
+
# If we have a time_metric, we'll track a relative time
|
70
|
+
# This will be used for "every" and "after that"
|
71
|
+
# Note: we default coeffecient to 1 for the case "Every day"
|
72
|
+
relative_time = nil # base
|
73
|
+
|
74
|
+
if elements[:time_metric] # "24 hours..."
|
75
|
+
relative_time = case elements[:time_metric_unit]
|
76
|
+
when "second", "minute", "hour", "day", "week", "fortnight", "month", "year"
|
77
|
+
(elements[:time_metric_coefficient] || 1).to_i.send(elements[:time_metric_unit]).ago
|
78
|
+
when "time"
|
79
|
+
Time.now # "Every time" = since now
|
80
|
+
else
|
81
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} referenced invalid time unit (e.g. days, hours, ..): #{elements[:time_metric_unit]}: #{letter_info}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Then, we'll base it on ourselves
|
86
|
+
# Select all items where not exists letter = self (unless recurring, then base on time)
|
87
|
+
if elements[:recurring].blank? # every
|
88
|
+
scope = Checks.non_recurring_check(scope: scope, source: source_arel, pigeon: pigeon_arel, letter: letter)
|
89
|
+
else # Recurring ("every")
|
90
|
+
scope = Checks.recurring_check(scope: scope, source: source_arel, pigeon: pigeon_arel, letter: letter, relative_time: relative_time)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Next, we're going to check the previous req clause
|
94
|
+
if elements[:relative] || ( elements[:joiner] == "then" )# "after..." or begins with "then..."
|
95
|
+
|
96
|
+
# TODO: Type may also be a milestone or item metric
|
97
|
+
# Need to make this much more diverse
|
98
|
+
relative_action = nil
|
99
|
+
time_item = nil # TODO: Event
|
100
|
+
|
101
|
+
relative_time = Time.now if relative_time.nil? # If we don't have a relative time, we count it against now
|
102
|
+
|
103
|
+
# Then, we need to check our relative
|
104
|
+
if elements[:relative]
|
105
|
+
case elements[:relative]
|
106
|
+
when "after"
|
107
|
+
relative_action = :after
|
108
|
+
time_item = elements[:time_item]
|
109
|
+
else
|
110
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} referenced invalid time relative (e.g. before/after): #{elements[:relative]}: #{letter_info}"
|
111
|
+
end
|
112
|
+
else # joiner - if we don't have a relative, we'll default to "after that"
|
113
|
+
case elements[:joiner]
|
114
|
+
when "then"
|
115
|
+
relative_action = :after
|
116
|
+
time_item = "that"
|
117
|
+
else
|
118
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} got to illegal area of code: #{letter_info}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Then, we need to find the relative letter type
|
123
|
+
if time_item == "that" # Compare with last letter -- this is baked in
|
124
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} referenced to previous letters (\"#{elements[:time_metric]} #{elements[:relative]}\"), but ambiguous previous lines: #{letter_info}" if previous_letters.empty?
|
125
|
+
scope = Checks.after_letter_check( scope: scope, source: source_arel, pigeon: pigeon_arel, letters: previous_letters.clone, relative_action: relative_action, relative_time: relative_time )
|
126
|
+
elsif time_item =~ /^(sign((ing )|[-]|[ ])?up)|(creat(e|ion))$/i # this is also baked in (matches: signup, signing up, sign up, sign-up, create, creation)
|
127
|
+
scope = scope.where("created_at < ?", relative_time)
|
128
|
+
else
|
129
|
+
# Let's try to match this against the events given to settings
|
130
|
+
matched_events = Settings.events.select do |event|
|
131
|
+
match = event[:matcher].match(time_item)
|
132
|
+
if match
|
133
|
+
# TODO # uhh
|
134
|
+
scope = event[:clause].call(scope, relative_time, match)
|
135
|
+
true
|
136
|
+
else
|
137
|
+
false
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} has unmatched event: \"#{time_item}\". You must set an extension to match all events: #{letter_info}" if matched_events.count == 0
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
# Last, we'll check conditionals brought on by us
|
147
|
+
if elements[:conditionals] # "who've..."
|
148
|
+
matched_conditionals = Settings.conditionals.select do |conditional|
|
149
|
+
match = conditional[:matcher].match(elements[:conditionals])
|
150
|
+
if match
|
151
|
+
scope = conditional[:clause].call(scope, match)
|
152
|
+
true
|
153
|
+
else
|
154
|
+
false
|
155
|
+
end
|
156
|
+
end
|
157
|
+
raise PigeonError::PigeonFlightConfigError, "Flight #{flight} has unmatched conditional: \"#{elements[:conditionals]}\". You must set an extension to match all conditionals: #{letter_info}" if matched_conditionals.count == 0
|
158
|
+
end
|
159
|
+
|
160
|
+
# Set previous letters
|
161
|
+
if elements[:base_element].nil? && !elements[:joiner].nil? && !elements[:conditionals].nil?
|
162
|
+
# then who've ...
|
163
|
+
previous_letters << letter # we're going to append ourselves to previous letters to allow us to skip since we've kept a running base element, but added conditions that make us skippable
|
164
|
+
else
|
165
|
+
# otherwise, to continue on, you must have received this specific letter
|
166
|
+
previous_letters = [ letter ] # just us
|
167
|
+
end
|
168
|
+
|
169
|
+
return {
|
170
|
+
scope: scope,
|
171
|
+
base_scope: base_scope,
|
172
|
+
letter: letter,
|
173
|
+
previous_letters: previous_letters,
|
174
|
+
entity: source_arel.name }
|
175
|
+
end
|
176
|
+
|
177
|
+
# This function takes a base element like "user" and returns a scope
|
178
|
+
def self.get_base_scope(base_element)
|
179
|
+
# First, we'll check the bases defined
|
180
|
+
Settings.bases.each do |base|
|
181
|
+
match = base[:matcher].match(base_element)
|
182
|
+
if match
|
183
|
+
res = base[:clause].call(match)
|
184
|
+
|
185
|
+
if !res.is_a?(ActiveRecord::Relation)
|
186
|
+
raise PigeonError::PigeonConfigError, "Base scope must return an ActiveRecord::Relation, but got #{res.class.name} for #{base_element}"
|
187
|
+
end
|
188
|
+
|
189
|
+
return res
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Then, we'll check for a model named likeso
|
194
|
+
# TODO/Note, we may want to depluralize
|
195
|
+
klass = Object.const_get(base_element.gsub(' ','_').classify) rescue nil
|
196
|
+
|
197
|
+
if klass && ( klass < ActiveRecord::Base )
|
198
|
+
return klass.scoped
|
199
|
+
end
|
200
|
+
|
201
|
+
raise PigeonError::PigeonConfigError, "Unable to find a scope for base element: #{base_element}"
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
end
|
data/lib/pigeons.rb
ADDED
data/pigeons.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pigeons/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "pigeons"
|
8
|
+
gem.version = Pigeons::VERSION
|
9
|
+
gem.authors = ["Geoff Hayes"]
|
10
|
+
gem.email = ["geoff@safeshepherd.com"]
|
11
|
+
gem.description = %q{Pigeons makes it a breeze to send your users lifecycle e-mails.}
|
12
|
+
gem.summary = %q{Pigeons provides an extensible way to send our lifecycle e-mails through simple human-readable syntax}
|
13
|
+
gem.homepage = "https://github.com/hayesgm/pigeons"
|
14
|
+
|
15
|
+
gem.add_dependency('activesupport')
|
16
|
+
gem.add_dependency('activerecord')
|
17
|
+
gem.add_dependency('actionmailer')
|
18
|
+
|
19
|
+
gem.add_development_dependency('mocha')
|
20
|
+
gem.add_development_dependency('shoulda')
|
21
|
+
gem.add_development_dependency('test-unit')
|
22
|
+
gem.add_development_dependency('rake')
|
23
|
+
gem.add_development_dependency('sqlite3')
|
24
|
+
|
25
|
+
gem.files = `git ls-files`.split($/)
|
26
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
27
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
28
|
+
gem.require_paths = ["lib"]
|
29
|
+
end
|
data/tasks/pigeons.rake
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
|
2
|
+
namespace :pigeons do
|
3
|
+
|
4
|
+
desc "Show all posts that would be immediately sent"
|
5
|
+
task :check, [ :debug ] => :environment do |t, args|
|
6
|
+
p [ 'Pigeons::', 'Rake::Pigeons::Check', 'Initiating', args ]
|
7
|
+
|
8
|
+
flights = Pigeons.assemble(send: false, debug: args[:debug] == "true")
|
9
|
+
|
10
|
+
flights.each do |flight, letters|
|
11
|
+
puts "\n#{flight.titleize} Flight"
|
12
|
+
|
13
|
+
letters.each do |letter_info|
|
14
|
+
puts "\t#{letter_info[:letter].titleize} -> #{letter_info[:count]} #{letter_info[:entity]}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
p [ 'Pigeons::', 'Rake::Pigeons::Check', 'Completed' ]
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Run a simulation by day of Pigeon letters to be sent."
|
22
|
+
task :flight_test, [ :days, :debug, :force ] => :environment do |t, args|
|
23
|
+
unless Rails.env.staging? || Rails.env.development? || Rails.env.test? # Note, due to mocks, etc. this is not considered safe to run on any environment besides develpoment and staging
|
24
|
+
unless args[:force].blank?
|
25
|
+
puts "\n\e[0;31m ######################################################################"
|
26
|
+
puts " #\n # Are you REALLY sure you want to run Flight Test in #{Rails.env.capitalize}?"
|
27
|
+
puts " #\n # Nothing should affect the database- but this is not considered safe for production databases."
|
28
|
+
puts " #\n # Specifically, we are going to override Time.now, Time.current (with monkeypatches) and run a"
|
29
|
+
puts " #\n # actual simulation day-by-day that we will rollback (and hope doesn't actually send letters)."
|
30
|
+
puts " #\n # These assumptions are nice, but not good in a production environment."
|
31
|
+
puts " #\n # Enter y/N + enter to continue\n #"
|
32
|
+
puts " ######################################################################\e[0m\n"
|
33
|
+
proceed = STDIN.gets[0..0] rescue nil
|
34
|
+
exit unless proceed == 'y' || proceed == 'Y'
|
35
|
+
else
|
36
|
+
raise "Refusing to run Flight Test on anything but Development and Staging (try rake pigeons:flight_test[days,debug,*force])"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
p [ 'Pigeons::', 'Rake::Pigeons::FlightTest', 'Initiating', args ]
|
41
|
+
days = args[:days] ? args[:days].to_i : 15 # Default to 15 days?
|
42
|
+
debug = args[:debug] || false
|
43
|
+
|
44
|
+
# Stub both Time.now and Time.current
|
45
|
+
reality = Time.now
|
46
|
+
current_reality = Time.current
|
47
|
+
|
48
|
+
results = {} # E.g. { 'aflight': [ { letter_a: 5 } ]}
|
49
|
+
benchmarks = [] # day => run_time
|
50
|
+
|
51
|
+
# TODO: I'd like to be able to do this without degrading the environment with stubs, but that's probably not going to happen
|
52
|
+
time_class = class << ::Time; self; end
|
53
|
+
pigeon_class = class << ::PigeonMailer; self; end
|
54
|
+
PigeonMailer.action_methods.each { |mailer_action| pigeon_class.send(:define_method, mailer_action) { |*args| return true } }
|
55
|
+
|
56
|
+
PigeonLetter.transaction do
|
57
|
+
begin
|
58
|
+
days.times do |day|
|
59
|
+
puts ''
|
60
|
+
puts '-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-'
|
61
|
+
puts "Pigeons:: Simulating Day ##{day+1}"
|
62
|
+
puts ''
|
63
|
+
puts 'Letters by Type [Totals]'
|
64
|
+
PigeonLetter.where("sent_at IS NOT NULL").to_a.group_by { |pl| pl.letter_type }.each { |letter_type, letters| puts "\t#{letter_type}: #{letters.count}" }
|
65
|
+
puts ''
|
66
|
+
puts ''
|
67
|
+
|
68
|
+
# Time is a social construct
|
69
|
+
# We'll adjust it as need be
|
70
|
+
time_class.send(:define_method, :now) { reality + day.days }
|
71
|
+
time_class.send(:define_method, :current) { current_reality + day.days }
|
72
|
+
|
73
|
+
start_time = Time.new.to_f
|
74
|
+
flights = Pigeons.assemble(send: true, debug: debug)
|
75
|
+
benchmarks[day] = Time.new.to_f - start_time
|
76
|
+
|
77
|
+
flights.each do |flight, letters|
|
78
|
+
results[flight] ||= {}
|
79
|
+
|
80
|
+
puts "\n#{flight.titleize} Flight"
|
81
|
+
|
82
|
+
letters.each do |letter_info|
|
83
|
+
results[flight][letter_info[:letter]] ||= []
|
84
|
+
results[flight][letter_info[:letter]][day] = ( results[flight][letter_info[:letter]][day] || 0 ) + letter_info[:count]
|
85
|
+
puts "\t#{letter_info[:letter].titleize} -> #{letter_info[:count]} #{letter_info[:entity]}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
rescue => e
|
90
|
+
p [ 'Pigeons::', 'Encountered error in Rake::Pigeons::FlightTest', e.inspect ]
|
91
|
+
puts e.backtrace.join("\n\t")
|
92
|
+
ensure
|
93
|
+
p [ 'Pigeons::', 'Rolling back any changes made during test flight...' ]
|
94
|
+
raise ActiveRecord::Rollback # Force a rollback-- we don't want anything to be real here. In the words of Biggie, "It was all a dream..."
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
p [ 'Pigeons::', 'Flight Test Results' ]
|
99
|
+
p [ 'Pigeons::' 'Raw Results', results ]
|
100
|
+
p [ 'Pigeons::' 'Runtimes', benchmarks ]
|
101
|
+
|
102
|
+
padding = proc { |num, i| " " + num.to_s + ( 1..(i.to_s.length - 1 + 2*3 - num.to_s.length) ).map { " " }.join }
|
103
|
+
|
104
|
+
results.each do |flight, day_by_day|
|
105
|
+
puts '##############################'
|
106
|
+
puts "# #{flight.capitalize} Flight #"
|
107
|
+
puts '##############################'
|
108
|
+
puts ""
|
109
|
+
puts ""
|
110
|
+
|
111
|
+
# " "
|
112
|
+
# Get the length just right
|
113
|
+
leading = ( 1..(day_by_day.map { |letter, count_by_day| letter }.sort { |a,b| a.length <=> b.length }.last.length + 2) ).map { " " }.join
|
114
|
+
|
115
|
+
|
116
|
+
puts "#{leading}| Day #{ (1..days).map { |i| "#{i} " }.join }"
|
117
|
+
day_by_day.each do |letter, count_by_day|
|
118
|
+
puts ""
|
119
|
+
puts "#{letter}#{ ( leading.length - letter.length ).times.map { " " }.join }| #{ count_by_day.each_with_index.map { |count, i| padding.call(count, i+1) }.join }"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
p [ 'Pigeons::', 'Rake::Pigeons::FlightTest', 'Completed' ]
|
124
|
+
end
|
125
|
+
|
126
|
+
task :send, [] => :environment do |t, args|
|
127
|
+
p [ 'Pigeons::', 'Rake::Pigeons::Send', 'Initiating', args ]
|
128
|
+
|
129
|
+
flights = Pigeons.assemble(send: true)
|
130
|
+
|
131
|
+
flights.each do |flight, letters|
|
132
|
+
puts "\n#{flight.titleize} Flight"
|
133
|
+
|
134
|
+
letters.each do |letter_info|
|
135
|
+
puts "\t#{letter_info[:letter].titleize} -> #{letter_info[:count]} #{letter_info[:entity]}"
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
p [ 'Pigeons::', 'Rake::Pigeons::Send', 'Completed' ]
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
ENV["RAILS_ENV"] = "test"
|
2
|
+
|
3
|
+
require File.expand_path('../test_helper', __FILE__)
|
4
|
+
require 'active_support/all'
|
5
|
+
require 'active_record'
|
6
|
+
require 'action_mailer'
|
7
|
+
require 'test/unit'
|
8
|
+
|
9
|
+
# Some defines expected by pigeons
|
10
|
+
class PigeonLetter < ActiveRecord::Base; end
|
11
|
+
class PigeonMailer < ActionMailer::Base; end
|
12
|
+
|
13
|
+
require 'pigeons'
|
14
|
+
require 'mocha'
|
15
|
+
include Mocha
|
16
|
+
|
17
|
+
::NOW = ::Time.now
|
18
|
+
::CURRENT = ::Time.current
|
19
|
+
|
20
|
+
::Time.stubs(now: ::NOW) # Make this static for a test
|
21
|
+
::Time.stubs(current: ::CURRENT)
|
22
|
+
|
23
|
+
ActiveRecord::Base.establish_connection(
|
24
|
+
:adapter => 'sqlite3',
|
25
|
+
:database => ':memory:'
|
26
|
+
)
|
27
|
+
|
28
|
+
ActiveRecord::Schema.define do
|
29
|
+
self.verbose = false
|
30
|
+
|
31
|
+
create_table :dragons, :force => true do |t|
|
32
|
+
t.string :property
|
33
|
+
t.string :color
|
34
|
+
t.boolean :eaten
|
35
|
+
t.boolean :slept
|
36
|
+
t.datetime :hatched_at
|
37
|
+
t.datetime "created_at", :null => false
|
38
|
+
t.datetime "updated_at", :null => false
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table :orcs, :force => true do |t|
|
42
|
+
t.string :name
|
43
|
+
t.datetime "created_at", :null => false
|
44
|
+
t.datetime "updated_at", :null => false
|
45
|
+
end
|
46
|
+
|
47
|
+
create_table :battles, :force => true do |t|
|
48
|
+
t.integer :dragon_id
|
49
|
+
t.integer :orc_id
|
50
|
+
t.boolean :is_dragon_victor
|
51
|
+
t.datetime "created_at", :null => false
|
52
|
+
t.datetime "updated_at", :null => false
|
53
|
+
end
|
54
|
+
|
55
|
+
create_table :pixies, :force => true do |t|
|
56
|
+
t.string :text
|
57
|
+
t.datetime "created_at", :null => false
|
58
|
+
t.datetime "updated_at", :null => false
|
59
|
+
end
|
60
|
+
|
61
|
+
create_table :levels, :force => true do |t|
|
62
|
+
t.integer :pixie_id
|
63
|
+
t.integer :level
|
64
|
+
t.datetime "created_at", :null => false
|
65
|
+
t.datetime "updated_at", :null => false
|
66
|
+
end
|
67
|
+
|
68
|
+
create_table :pigeon_letters, :force => true do |t|
|
69
|
+
t.string "letter_type"
|
70
|
+
t.string "cargo_type"
|
71
|
+
t.integer "cargo_id"
|
72
|
+
t.datetime "sent_at"
|
73
|
+
t.datetime "created_at", :null => false
|
74
|
+
t.datetime "updated_at", :null => false
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
# Clear these out in case they were used by any project settings
|
80
|
+
Pigeons::Settings.bases = []
|
81
|
+
Pigeons::Settings.conditionals = []
|
82
|
+
Pigeons::Settings.events = []
|
83
|
+
|
84
|
+
# Let's check classify works nicely
|
85
|
+
ActiveSupport::Inflector.inflections do |inflect|
|
86
|
+
inflect.irregular 'pixie', 'pixies'
|
87
|
+
end
|
88
|
+
|
89
|
+
# This is the scope that's going to be on every object by the very nature of pigeons
|
90
|
+
# Thus, we'll make helpers for them
|
91
|
+
def letter_not_exists(scope, letter_type)
|
92
|
+
pigeon_arel = PigeonLetter.arel_table
|
93
|
+
source_arel = scope.arel_table
|
94
|
+
|
95
|
+
scope.where(
|
96
|
+
PigeonLetter.where(
|
97
|
+
pigeon_arel[:cargo_id].eq(source_arel[:id]).and(
|
98
|
+
pigeon_arel[:cargo_type].eq(source_arel.name.classify)
|
99
|
+
).and(
|
100
|
+
pigeon_arel[:created_at].not_eq(nil)
|
101
|
+
).and(
|
102
|
+
pigeon_arel[:letter_type].eq(letter_type.to_s)
|
103
|
+
)
|
104
|
+
).exists.not
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
def simple_scope(scope, count=1, id=0)
|
109
|
+
pigeon_arel = PigeonLetter.arel_table
|
110
|
+
source_arel = scope.arel_table
|
111
|
+
|
112
|
+
scope.where("id%#{count}=#{id}").where(
|
113
|
+
PigeonLetter.where(
|
114
|
+
pigeon_arel[:cargo_id].eq(source_arel[:id]).and(
|
115
|
+
pigeon_arel[:cargo_type].eq(source_arel.name.classify)
|
116
|
+
).and(
|
117
|
+
pigeon_arel[:created_at].gt(Pigeons::Settings.cooldown.ago)
|
118
|
+
)
|
119
|
+
).exists.not
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
class Dragon < ActiveRecord::Base
|
124
|
+
has_many :battles
|
125
|
+
end
|
126
|
+
|
127
|
+
class Orc < ActiveRecord::Base
|
128
|
+
has_many :battles
|
129
|
+
end
|
130
|
+
|
131
|
+
class Battle < ActiveRecord::Base
|
132
|
+
belongs_to :dragon
|
133
|
+
belongs_to :orc
|
134
|
+
end
|
135
|
+
|
136
|
+
class Pixie < ActiveRecord::Base
|
137
|
+
has_many :levels
|
138
|
+
end
|
139
|
+
|
140
|
+
class Level < ActiveRecord::Base
|
141
|
+
belongs_to :pixie
|
142
|
+
end
|
143
|
+
|