hobo 0.8.8 → 0.8.9

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