hobo 0.8.8 → 0.8.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 (58) hide show
  1. data/CHANGES.txt +34 -0
  2. data/Rakefile +30 -24
  3. data/bin/hobo +30 -10
  4. data/doctest/hobo/hobo_helper.rdoctest +92 -0
  5. data/doctest/hobo/lifecycles.rdoctest +261 -0
  6. data/doctest/scopes.rdoctest +387 -0
  7. data/dryml_generators/rapid/forms.dryml.erb +3 -3
  8. data/dryml_generators/rapid/pages.dryml.erb +4 -4
  9. data/lib/active_record/viewhints_validations_interceptor.rb +1 -1
  10. data/lib/hobo.rb +1 -1
  11. data/lib/hobo/accessible_associations.rb +3 -3
  12. data/lib/hobo/authentication_support.rb +1 -1
  13. data/lib/hobo/dryml.rb +10 -0
  14. data/lib/hobo/dryml/taglib.rb +3 -5
  15. data/lib/hobo/hobo_helper.rb +3 -1
  16. data/lib/hobo/include_in_save.rb +1 -0
  17. data/lib/hobo/lifecycles/actions.rb +6 -2
  18. data/lib/hobo/model.rb +1 -1
  19. data/lib/hobo/model_controller.rb +34 -12
  20. data/lib/hobo/permissions.rb +1 -1
  21. data/lib/hobo/rapid_helper.rb +3 -0
  22. data/lib/hobo/scopes/association_proxy_extensions.rb +8 -2
  23. data/lib/hobo/scopes/automatic_scopes.rb +3 -3
  24. data/lib/hobo/user_controller.rb +2 -1
  25. data/rails_generators/hobo/hobo_generator.rb +1 -1
  26. data/rails_generators/hobo/templates/application.dryml +0 -2
  27. data/rails_generators/hobo_admin_site/hobo_admin_site_generator.rb +45 -0
  28. data/rails_generators/hobo_admin_site/templates/admin.css +2 -0
  29. data/rails_generators/hobo_admin_site/templates/application.dryml +1 -0
  30. data/rails_generators/hobo_admin_site/templates/controller.rb +13 -0
  31. data/rails_generators/hobo_admin_site/templates/site_taglib.dryml +32 -0
  32. data/rails_generators/hobo_admin_site/templates/users_index.dryml +5 -0
  33. data/rails_generators/hobo_front_controller/hobo_front_controller_generator.rb +7 -1
  34. data/rails_generators/hobo_front_controller/templates/index.dryml +16 -0
  35. data/rails_generators/hobo_rapid/hobo_rapid_generator.rb +31 -1
  36. data/rails_generators/hobo_rapid/templates/hobo-rapid.js +5 -3
  37. data/rails_generators/hobo_rapid/templates/lowpro.js +40 -21
  38. data/rails_generators/hobo_rapid/templates/themes/clean/public/images/101-3B5F87-ACD3E6.png +0 -0
  39. data/rails_generators/hobo_rapid/templates/themes/clean/public/images/30-3E547A-242E42.png +0 -0
  40. data/rails_generators/hobo_rapid/templates/themes/clean/public/images/30-DBE1E5-FCFEF5.png +0 -0
  41. data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/clean.css +12 -4
  42. data/rails_generators/hobo_subsite/hobo_subsite_generator.rb +1 -1
  43. data/rails_generators/hobo_user_controller/hobo_user_controller_generator.rb +22 -0
  44. data/rails_generators/hobo_user_controller/templates/accept_invitation.dryml +5 -0
  45. data/rails_generators/hobo_user_controller/templates/controller.rb +22 -0
  46. data/rails_generators/hobo_user_model/hobo_user_model_generator.rb +17 -1
  47. data/rails_generators/hobo_user_model/templates/invite.erb +9 -0
  48. data/rails_generators/hobo_user_model/templates/mailer.rb +15 -0
  49. data/rails_generators/hobo_user_model/templates/model.rb +31 -4
  50. data/taglibs/rapid_core.dryml +25 -6
  51. data/taglibs/rapid_forms.dryml +65 -24
  52. data/taglibs/rapid_lifecycles.dryml +1 -1
  53. data/taglibs/rapid_navigation.dryml +2 -2
  54. data/taglibs/rapid_plus.dryml +4 -3
  55. metadata +151 -210
  56. data/Manifest +0 -155
  57. data/hobo.gemspec +0 -46
  58. data/rails_generators/hobo_rapid/templates/themes/clean/public/images/100-3B5F87-ACD3E6.png +0 -0
data/CHANGES.txt CHANGED
@@ -1,3 +1,37 @@
1
+ === Hobo 0.8.9 ===
2
+
3
+ Enhancements:
4
+
5
+ -
6
+ [precompile_taglibs](http://groups.google.com/group/hobousers/browse_thread/thread/29694e75f60c0870/6b05f75f2f7e91f5)
7
+ allows you to precompile taglibs during application startup rather
8
+ than on demand.
9
+
10
+ - `--invite-only` options added ti generator
11
+
12
+ Major bug fixes:
13
+
14
+ - [Bug
15
+ 461](https://hobo.lighthouseapp.com/projects/8324-hobo/tickets/461-hobo-is-not-compatible-with-firefox-35):
16
+ Firefox 3.5 problems were caused by lowpro. For existing projects,
17
+ you will have to update your copy of [public/javascripts/lowpro.js](http://github.com/tablatom/hobo/raw/master/hobo/rails_generators/hobo_rapid/templates/lowpro.js)
18
+
19
+ - [Bug
20
+ 477](http://groups.google.com/group/hobousers/browse_thread/thread/5a15288f9703a8a4/58a8dee62b237d29)
21
+ caused problems when the user submitted a form from the index page.
22
+
23
+ - "collection" was renamed to "collection-heading" in the Rapid
24
+ generated show-page.
25
+
26
+ - [Bug
27
+ 473](https://hobo.lighthouseapp.com/projects/8324/tickets/473-use-timezonenow-instead-of-timenow#ticket-473-5):
28
+ Hobo now uses any time zone's configured for the application rather
29
+ than using the server's time zone.
30
+
31
+ Minor bug fixes and enhancements:
32
+
33
+ See the [github log](http://github.com/bryanlarsen/hobo/commits/v0.8.5)
34
+
1
35
  === Hobo 0.8.8 ===
2
36
 
3
37
  Hobo 0.8.8 comes with some slight changes to the colour scheme for the
data/Rakefile CHANGED
@@ -2,6 +2,13 @@ require 'rake'
2
2
  require 'rake/rdoctask'
3
3
  require 'rake/testtask'
4
4
 
5
+ require 'activerecord'
6
+ ActiveRecord::ActiveRecordError # hack for https://rails.lighthouseapp.com/projects/8994/tickets/2577-when-using-activerecordassociations-outside-of-rails-a-nameerror-is-thrown
7
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/lib')
8
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobofields/lib')
9
+ $:.unshift File.join(File.expand_path(File.dirname(__FILE__)), '/../hobosupport/lib')
10
+ require 'hobo'
11
+
5
12
  desc "Default Task"
6
13
  task :default => [ :test ]
7
14
 
@@ -35,28 +42,27 @@ Rake::RDocTask.new(:rdoc) do |rdoc|
35
42
  end
36
43
 
37
44
 
38
- # --- Packaging and Rubyforge --- #
39
-
40
- require 'echoe'
41
-
42
- Echoe.new('hobo') do |p|
43
- p.author = "Tom Locke"
44
- p.email = "tom@tomlocke.com"
45
- p.summary = "The web app builder for Rails"
46
- p.url = "http://hobocentral.net/"
47
- p.project = "hobo"
48
-
49
- p.changelog = "CHANGES.txt"
50
- p.version = "0.8.8"
51
-
52
- p.dependencies = [
53
- 'hobosupport =0.8.8',
54
- 'hobofields =0.8.8',
55
- 'rails >=2.2.2',
56
- 'mislav-will_paginate >=2.2.1']
57
-
58
- p.development_dependencies = []
45
+ # --- Packaging and Rubyforge & gemcutter & github--- #
46
+
47
+ begin
48
+ require 'jeweler'
49
+ Jeweler::Tasks.new do |gemspec|
50
+ gemspec.version = Hobo::VERSION
51
+ gemspec.name = "hobo"
52
+ gemspec.email = "tom@tomlocke.com"
53
+ gemspec.summary = "The web app builder for Rails"
54
+ gemspec.homepage = "http://hobocentral.net/"
55
+ gemspec.authors = ["Tom Locke"]
56
+ gemspec.rubyforge_project = "hobo"
57
+ gemspec.add_dependency("rails", [">= 2.2.2"])
58
+ gemspec.add_dependency("mislav-will_paginate", [">= 2.2.1"])
59
+ gemspec.add_dependency("hobosupport", ["= #{Hobo::VERSION}"])
60
+ gemspec.add_dependency("hobofields", ["= #{Hobo::VERSION}"])
61
+ end
62
+ Jeweler::GemcutterTasks.new
63
+ Jeweler::RubyforgeTasks.new do |rubyforge|
64
+ rubyforge.doc_task = false
65
+ end
66
+ rescue LoadError
67
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
59
68
  end
60
-
61
-
62
-
data/bin/hobo CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'fileutils'
4
+ require 'pathname'
5
+ require 'activesupport'
6
+
4
7
  Signal.trap("INT") { puts; exit }
5
8
 
6
9
  hobo_src = File.join(File.dirname(__FILE__), "../hobo_files/plugin")
@@ -14,6 +17,7 @@ Options:
14
17
  -d | --database <database> # e.g. mysql, sqlite
15
18
  -r | --rails <version> # rails version to use
16
19
  -n | --no-rails # don't run 'rails'. Assumes app-path=='.'
20
+ --invite-only # add features for an invite-ony website (admin site, no signup)
17
21
  "
18
22
 
19
23
 
@@ -36,11 +40,10 @@ def command(*s)
36
40
  exit(1) unless ok
37
41
  end
38
42
 
39
- user_model = "user"
40
- create_db = false
41
- run_rails = true
42
-
43
- puts ARGV
43
+ user_model = "user"
44
+ create_db = false
45
+ run_rails = true
46
+ invite_ony = ""
44
47
 
45
48
  while true
46
49
  case arg_name = ARGV.shift
@@ -50,13 +53,16 @@ while true
50
53
  when "--db-create"
51
54
  create_db = true
52
55
  when "--hobo-src"
53
- hobo_src = "../" + ARGV.shift
56
+ hobo_src = ARGV.shift
57
+ hobo_src = "../" + hobo_src unless Pathname.new(hobo_src).absolute?
54
58
  when "-d", "--database"
55
59
  database_type = ARGV.shift
56
60
  when "-r", "--rails"
57
61
  rails_version = ARGV.shift
58
62
  when "-n", "--no-rails"
59
63
  run_rails = false
64
+ when "--invite-only"
65
+ invite_ony = "--invite-only"
60
66
  when "--help"
61
67
  puts USAGE
62
68
  exit
@@ -100,16 +106,16 @@ Dir.chdir(app_path) do
100
106
  command(gen, "hobo --add-gem --add-routes")
101
107
 
102
108
  puts "\nInstalling Hobo Rapid and default theme...\n"
103
- command("#{gen} hobo_rapid --import-tags")
109
+ command("#{gen} hobo_rapid --import-tags #{invite_ony}")
104
110
 
105
111
  if user_model
106
112
  puts "\nCreating #{user_model} model and controller...\n"
107
- command("#{gen} hobo_user_model #{user_model}")
108
- command("#{gen} hobo_user_controller #{user_model}")
113
+ command("#{gen} hobo_user_model #{user_model} #{invite_ony}")
114
+ command("#{gen} hobo_user_controller #{user_model} #{invite_ony}")
109
115
  end
110
116
 
111
117
  puts "\nCreating standard pages...\n"
112
- command("#{gen} hobo_front_controller front --delete-index --add-routes")
118
+ command("#{gen} hobo_front_controller front --delete-index --add-routes #{invite_ony}")
113
119
 
114
120
  if create_db
115
121
  puts "\nCreating databases"
@@ -117,3 +123,17 @@ Dir.chdir(app_path) do
117
123
  end
118
124
  end
119
125
 
126
+ if invite_ony.present?
127
+ puts %(
128
+ Invite-only website
129
+ If you wish to prevent all access to the site to non-members, add 'before_filter :login_required'
130
+ to the relevant controllers, e.g. to prevent all access to the site, add
131
+
132
+ include Hobo::AuthenticationSupport
133
+ before_filter :login_required
134
+
135
+ to application_controller.rb (note that the include statement is not required for hobo_controllers)
136
+
137
+ NOTE: You might want to sign up as the administrator before adding this!
138
+ )
139
+ end
@@ -0,0 +1,92 @@
1
+ # Hobo::HoboHelper
2
+
3
+ Various view helpers
4
+
5
+ doctest_require: 'rubygems'
6
+ doctest_require: 'active_support'
7
+ doctest_require: 'mocha'
8
+ doctest_require: '../../../hobosupport/lib/hobosupport'
9
+ doctest_require: '../../../hobofields/lib/hobofields'
10
+ doctest_require: '../../lib/hobo'
11
+
12
+ Create a mock view layer:
13
+
14
+ >>
15
+ class View
16
+ extend Hobo::HoboHelper
17
+ class << self
18
+ protected_instance_methods.each {|m| public m }
19
+
20
+ def params; {} ;end
21
+ def subsite; "" ;end
22
+ def base_url; "" ;end
23
+ end
24
+ end
25
+ >> RAILS_ROOT = "test-app"
26
+
27
+ Useful stuff
28
+
29
+ >> def init_mocha; $stubba = Mocha::Central.new; end
30
+ >>
31
+ class Thing
32
+ class Mocks; extend Mocha::AutoVerify; end
33
+ def self.mock(hash)
34
+ Mocks.mock(hash.update(:class => self))
35
+ end
36
+ end
37
+
38
+
39
+ ## `object_url`
40
+
41
+ Returns a canonical restful URL for a given object. THe Hobo routing is checked and URLs are only returned for routes that exist.
42
+
43
+ Note that `object_url` doesn't perform "reverse routing". It knows nothing about attractive URLs you may have declared in your routes file.
44
+
45
+ Something to link to:
46
+
47
+ >> init_mocha
48
+ >> thing = Thing.mock(:to_url_path => "things/1")
49
+
50
+ ### Simple 'show' URLs
51
+
52
+ >> Hobo::ModelRouter.expects(:linkable?).with(Thing, :show, {:subsite => ''}).returns(true)
53
+ >> View.object_url(thing)
54
+ => "/things/1"
55
+
56
+ Returns nil if ModelRouter says it's not linkable
57
+
58
+ >> Hobo::ModelRouter.expects(:linkable?).with(Thing, :show, {:subsite => ''}).returns(false)
59
+ >> View.object_url(thing)
60
+ => nil
61
+
62
+ A URL to the 'edit' page:
63
+
64
+ >> Hobo::ModelRouter.expects(:linkable?).with(Thing, :edit, {:subsite => ''}).returns(true)
65
+ >> View.object_url(thing, :edit)
66
+ => "/things/1/edit"
67
+
68
+
69
+ ### POST URLs for creating new items in collections:
70
+
71
+ >> collection = mock(:origin => thing, :origin_attribute => "parts")
72
+ >> Hobo::ModelRouter.expects(:linkable?).with(Thing, :create_part, {:subsite => '', :method => :post}).returns(true)
73
+ >> View.object_url(collection, :method => :post)
74
+ => "/things/1/parts"
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
@@ -0,0 +1,261 @@
1
+ # Hobo Lifecycles
2
+
3
+ In the REST style, which is popular with Rails coders, we view our objects a bit like documents: you can post them to a website, get them again later, make changes to them and delete them. Of course, these objects also have behaviour, which we typically implement by hooking functionality to the create / update / delete events (e.g. using callbacks such as `after_create` in ActiveRecord).
4
+
5
+ This works great for many situations, but some objects are *not* best thought of as documents that we create and edit. In particular, web apps often contain objects that model some kind of *process*. A good example is *friendship* in a social app. Here's a description of how friendship might work:
6
+
7
+ * Any user can **request** friendship with another user
8
+ * The other user can **accept** or **reject** (or perhaps **ignore**) the request.
9
+ * The friendship is only **active** once it's been accepted
10
+ * An active friendship can be **cancelled** by either user.
11
+
12
+ Not a create, update or delete in sight. Those bold words capture the way we think about the friendship much better. Of course we *could* implement friendship in a RESTful style, but we'd be doing just that -- *implementing* it, not *declaring* it. The life-cycle of the friendship would be hidden in our code, scattered across a bunch of callbacks, permission methods and state variables. Experience has shown this type of code to be tedious to write, *extremely* error prone and fragile when changing.
13
+
14
+ Hobo lifecycles is a mechanism for declaring the life-cycle of a model in a natural manner. It's a bit like `acts_as_state_machine`, but Hobo-style :-)
15
+
16
+ First the junk to get us started:
17
+
18
+ doctest_require: 'rubygems'
19
+ doctest_require: 'activerecord'
20
+ >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobo/lib'
21
+ >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobo/lib'
22
+ >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobofields/lib'
23
+ >> $:.unshift '/home/blarsen/dev/agility-master/vendor/plugins/hobo/hobosupport/lib'
24
+ >> require 'hobo'
25
+ >> require 'hobo/model'
26
+ >> ActiveRecord::Base.establish_connection(:adapter => "sqlite3",
27
+ :database => "lifecycle_doctest")
28
+
29
+
30
+ A user model for our example:
31
+
32
+ >>
33
+ class User < ActiveRecord::Base
34
+ hobo_model
35
+ fields do
36
+ name :string
37
+ end
38
+ end
39
+
40
+ Now the friendship. For now we'll just declare the *invite* part of the lifecycle. We first declare the *states* -- there's only one for now. We then declare the *invite* action. This is the action that first creates the friendship, so we declare it with `create`:
41
+
42
+ >>
43
+ class Friendship < ActiveRecord::Base
44
+ hobo_model
45
+ belongs_to :requester, :class_name => "User"
46
+ belongs_to :requestee, :class_name => "User"
47
+
48
+ lifecycle do
49
+ state :requested
50
+ create :request, :params => [ :requestee ], :become => :requested, :user_becomes => :requester
51
+ end
52
+ end
53
+
54
+ Now let's get the DB ready:
55
+
56
+ doctest_require: '../../../hobofields/lib/hobo_fields/migration_generator'
57
+ >> up, down = HoboFields::MigrationGenerator.run
58
+ >> ActiveRecord::Migration.class_eval up
59
+ >> User.delete_all
60
+ >> Friendship.delete_all
61
+
62
+ We need some users to be friends:
63
+
64
+ >> tom = User.create(:name => "Tom")
65
+ >> bob = User.create(:name => "Bob")
66
+
67
+ Tom is allowed to request friendship:
68
+
69
+ >> Friendship::Lifecycle.can_request?(tom)
70
+ => true
71
+
72
+ Tom does so:
73
+
74
+ >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
75
+ >> f.requester.name
76
+ => "Tom"
77
+ >> f.requestee.name
78
+ => "Bob"
79
+ >> f.state
80
+ => "requested"
81
+
82
+ To continue modeling the friendship lifecycle, we add some *transitions*:
83
+
84
+ >>
85
+ class Friendship
86
+ lifecycle do
87
+ state :active
88
+ transition :accept, { :requested => :active}, :available_to => :requestee
89
+ transition :reject, { :requested => :destroy}, :available_to => :requestee
90
+ end
91
+ end
92
+
93
+ Note:
94
+
95
+ * Part of the transition declaration is *who* that transition is for. These two were only for the `requestee`:
96
+
97
+ * `:destroy` is a special pseudo state: entering this state causes the record to be destroyed
98
+
99
+ We can test which transitions are available:
100
+
101
+ >> f.lifecycle.available_transitions_for(tom).*.name
102
+ => []
103
+ >> f.lifecycle.available_transitions_for(bob).*.name
104
+ => ["accept", "reject"]
105
+ >> f.lifecycle.can_accept?(tom)
106
+ => false
107
+ >> f.lifecycle.can_reject?(tom)
108
+ => false
109
+ >> f.lifecycle.can_accept?(bob)
110
+ => true
111
+ >> f.lifecycle.can_reject?(bob)
112
+ => true
113
+
114
+ Accept the friendship
115
+
116
+ >> f.lifecycle.accept(bob)
117
+ >> f.state
118
+ => "active"
119
+
120
+ And now there's nowhere to go:
121
+
122
+ >> f.lifecycle.available_transitions_for(tom).*.name
123
+ => []
124
+ >> f.lifecycle.available_transitions_for(bob).*.name
125
+ => []
126
+
127
+ Cleanup
128
+
129
+ >> Friendship.delete_all
130
+
131
+ Let's try a rejected friendship:
132
+
133
+ >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
134
+ >> f.state
135
+ => "requested"
136
+ >> f.lifecycle.can_reject?(bob)
137
+ => true
138
+ >> Friendship.count
139
+ => 1
140
+ >> f.lifecycle.reject(bob)
141
+ >> Friendship.count
142
+ => 0
143
+
144
+ Cleanup
145
+
146
+ >> User.delete_all
147
+ >> Friendship.delete_all
148
+ >> Friendship::Lifecycle.reset
149
+
150
+ ## A bigger example
151
+
152
+ We'll run through the same example again, but we'll add some features
153
+
154
+ Transitions and states can have actions associated with them. A common use might be to send an email. We'll simulate that with a global variable `$emails`
155
+
156
+ >> $emails = []
157
+
158
+ We'll extend the lifecycle to allow:
159
+
160
+ * the requester to back out of the request
161
+
162
+ * the requestee to ignore the request
163
+
164
+ * either party to cancel the active friendship
165
+
166
+ Here is the entire lifecycle
167
+
168
+ >>
169
+ class Friendship < ActiveRecord::Base
170
+ hobo_model
171
+ belongs_to :requester, :class_name => "User"
172
+ belongs_to :requestee, :class_name => "User"
173
+
174
+ lifecycle do
175
+ state :requested, :active, :ignored
176
+
177
+ create :requester, :request, :params => [ :requestee ], :become => :requested do
178
+ $emails << "Dear #{requestee.name}, #{requester.name} wants to be friends with you"
179
+ end
180
+
181
+
182
+ transition :requestee, :accept, { :requested => :active } do
183
+ $emails << "Dear #{requester.name}, #{requestee.name} is now your friend :-)"
184
+ end
185
+
186
+ transition :requestee, :reject, { :requested => :destroy } do
187
+ $emails << "Dear #{requester.name}, #{requestee.name} blew you out :-("
188
+ end
189
+
190
+ transition :requestee, :ignore, { :requested => :ignored }
191
+
192
+ transition :requester, :retract, { :requested => :destroy } do
193
+ $emails << "Dear #{requestee.name}, #{requester.name} reconsidered"
194
+ end
195
+
196
+ transition [ :requester, :requestee ], :cancel, { :active => :destroy }
197
+ # TODO: send the email - for this we need the acting user to be passed to the block
198
+
199
+ end
200
+
201
+ end
202
+
203
+ Check the simple accept still works, and sends emails
204
+
205
+ >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
206
+ >> $emails.last
207
+ => "Dear Bob, Tom wants to be friends with you"
208
+ >> f.lifecycle.accept(bob)
209
+ >> $emails.last
210
+ => "Dear Tom, Bob is now your friend :-)"
211
+ >> f.lifecycle.active?
212
+ => true
213
+
214
+ Rejection:
215
+
216
+ >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
217
+ >> f.lifecycle.reject(bob)
218
+ >> $emails.last
219
+ => "Dear Tom, Bob blew you out :-("
220
+ >> f.state
221
+ => "destroy"
222
+
223
+ Retraction:
224
+
225
+ >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
226
+ >> f.lifecycle.can_retract?(bob)
227
+ => false
228
+ >> f.lifecycle.retract(tom)
229
+ >> $emails.last
230
+ => "Dear Bob, Tom reconsidered"
231
+ >> f.lifecycle.active?
232
+
233
+ Ignoring
234
+
235
+ >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
236
+ >> $emails = []
237
+ >> f.lifecycle.ignore(bob)
238
+ >> $emails # Ignoring shouldn't send any email
239
+ => []
240
+ >> f.state
241
+ => "ignored"
242
+
243
+ Requester cancels
244
+
245
+ >> f = Friendship::Lifecycle.request(tom, :requestee => bob)
246
+ >> f.lifecycle.can_cancel?(tom)
247
+ => false
248
+ >> f.lifecycle.accept(bob)
249
+ >> f.lifecycle.cancel(tom)
250
+ >> f.state
251
+ => "destroy"
252
+
253
+
254
+
255
+
256
+
257
+
258
+
259
+
260
+
261
+