pigeons 0.0.1pre
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|