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
@@ -10,6 +10,22 @@
10
10
  <ul>
11
11
  <li>To customise this page: edit app/views/front/index.dryml</li>
12
12
  </ul>
13
+
14
+ <%% if User.count == 0 -%>
15
+ <h3 style="margin-top: 20px;">There are no user accounts - please provide the details of the site administrator</h3>
16
+ <% if invite_only? %>
17
+ <form with="&this || User.new" without-cancel>
18
+ <field-list: fields="name, email_address, password, password_confirmation"/>
19
+ <submit: label="Register Administrator"/>
20
+ </form>
21
+ <% else -%>
22
+ <do with="&User.new"><%% this.exempt_from_edit_checks = true %>
23
+ <signup-form/>
24
+ </do>
25
+ <% end -%>
26
+ <%% end -%>
27
+
28
+
13
29
  </section>
14
30
  </header>
15
31
 
@@ -19,6 +19,12 @@ class HoboRapidGenerator < Hobo::Generator
19
19
  m.file "hobo-rapid.css", "public/stylesheets/hobo-rapid.css"
20
20
  create_all(m, "themes/clean/public", "public/hobothemes/clean")
21
21
  create_all(m, "themes/clean/views", "app/views/taglibs/themes/clean")
22
+
23
+ if with_admin_site?
24
+ options = ["--make-front-site", "admin"]
25
+ options.unshift "--invite-only" if invite_only?
26
+ m.dependency 'hobo_admin_site', options
27
+ end
22
28
  end
23
29
  end
24
30
 
@@ -34,6 +40,18 @@ class HoboRapidGenerator < Hobo::Generator
34
40
  <set-theme name="clean"/>
35
41
  )
36
42
 
43
+ if with_admin_site?
44
+ tag += %(
45
+ <extend tag="page">
46
+ <old-page merge>
47
+ <footer:>
48
+ <a href="&admin_users_url" if="&current_user.administrator?">Admin</a>
49
+ </footer:>
50
+ </old-page>
51
+ </extend>
52
+ )
53
+ end
54
+
37
55
  src = File.read(path)
38
56
  return if src.include?(tag)
39
57
 
@@ -47,9 +65,17 @@ class HoboRapidGenerator < Hobo::Generator
47
65
  end
48
66
 
49
67
 
68
+ def with_admin_site?
69
+ options[:admin]
70
+ end
71
+
72
+ def invite_only?
73
+ options[:admin] == :invite_only
74
+ end
75
+
50
76
  protected
51
77
  def banner
52
- "Usage: #{$0} #{spec.name} [--import-tags]"
78
+ "Usage: #{$0} #{spec.name} [--import-tags] [--admin | --invite-only]"
53
79
  end
54
80
 
55
81
  def add_options!(opt)
@@ -59,5 +85,9 @@ class HoboRapidGenerator < Hobo::Generator
59
85
  "Modify taglibs/application.dryml to import hobo-rapid and theme tags ") do |v|
60
86
  options[:import_tags] = true
61
87
  end
88
+ opt.on("--admin",
89
+ "Generate an admin subsite") { |v| options[:admin] = true }
90
+ opt.on("--invite-only",
91
+ "Generate an admin subsite with features for an invite only app") { |v| options[:admin] = :invite_only }
62
92
  end
63
93
  end
@@ -628,9 +628,11 @@ new HoboBehavior("ul.input-many", {
628
628
 
629
629
  this.element.selectChildren('li').each(function(li, index) {
630
630
  li.select('*[name]').each(function(control) {
631
- var changeId = control.id == control.name
632
- control.name = control.name.sub(new RegExp("^" + RegExp.escape(prefix) + "\[[0-9]+\]"), prefix + '[' + index +']')
633
- if (changeId) control.id = control.name
631
+ if(control.name) {
632
+ var changeId = control.id == control.name;
633
+ control.name = control.name.sub(new RegExp("^" + RegExp.escape(prefix) + "\[[0-9]+\]"), prefix + '[' + index +']');
634
+ if (changeId) control.id = control.name;
635
+ }
634
636
  })
635
637
  })
636
638
  }
@@ -2,7 +2,7 @@ LowPro = {};
2
2
  LowPro.Version = '0.5';
3
3
  LowPro.CompatibleWithPrototype = '1.6';
4
4
 
5
- if (Prototype.Version.indexOf(LowPro.CompatibleWithPrototype) != 0 && console && console.warn)
5
+ if (Prototype.Version.indexOf(LowPro.CompatibleWithPrototype) != 0 && window.console && window.console.warn)
6
6
  console.warn("This version of Low Pro is tested with Prototype " + LowPro.CompatibleWithPrototype +
7
7
  " it may not work as expected with this version (" + Prototype.Version + ")");
8
8
 
@@ -15,19 +15,19 @@ DOM = {};
15
15
  // DOMBuilder for prototype
16
16
  DOM.Builder = {
17
17
  tagFunc : function(tag) {
18
- return function() {
19
- var attrs, children;
20
- if (arguments.length>0) {
21
- if (arguments[0].nodeName ||
22
- typeof arguments[0] == "string")
23
- children = arguments;
24
- else {
25
- attrs = arguments[0];
26
- children = Array.prototype.slice.call(arguments, 1);
27
- };
28
- }
29
- return DOM.Builder.create(tag, attrs, children);
30
- };
18
+ return function() {
19
+ var attrs, children;
20
+ if (arguments.length>0) {
21
+ if (arguments[0].constructor == Object) {
22
+ attrs = arguments[0];
23
+ children = Array.prototype.slice.call(arguments, 1);
24
+ } else {
25
+ children = arguments;
26
+ };
27
+ children = $A(children).flatten()
28
+ }
29
+ return DOM.Builder.create(tag, attrs, children);
30
+ };
31
31
  },
32
32
  create : function(tag, attrs, children) {
33
33
  attrs = attrs || {}; children = children || []; tag = tag.toLowerCase();
@@ -109,6 +109,14 @@ Event.addBehavior = function(rules) {
109
109
 
110
110
  };
111
111
 
112
+ Event.delegate = function(rules) {
113
+ return function(e) {
114
+ var element = $(e.element());
115
+ for (var selector in rules)
116
+ if (element.match(selector)) return rules[selector].apply(this, $A(arguments));
117
+ }
118
+ }
119
+
112
120
  Object.extend(Event.addBehavior, {
113
121
  rules : {}, cache : [],
114
122
  reassignAfterAjax : false,
@@ -122,9 +130,9 @@ Object.extend(Event.addBehavior, {
122
130
  var match = sel.match(/^([^:]*)(?::(.*)$)?/), css = match[1], event = match[2];
123
131
  $$(css).each(function(element) {
124
132
  if (event) {
125
- observer = Event.addBehavior._wrapObserver(observer);
126
- $(element).observe(event, observer);
127
- Event.addBehavior.cache.push([element, event, observer]);
133
+ var wrappedObserver = Event.addBehavior._wrapObserver(observer);
134
+ $(element).observe(event, wrappedObserver);
135
+ Event.addBehavior.cache.push([element, event, wrappedObserver]);
128
136
  } else {
129
137
  if (!element.$$assigned || !element.$$assigned.include(observer)) {
130
138
  if (observer.attach) observer.attach(element);
@@ -198,7 +206,6 @@ var Behavior = {
198
206
  parent = properties.shift();
199
207
 
200
208
  var behavior = function() {
201
- var behavior = arguments.callee;
202
209
  if (!this.initialize) {
203
210
  var args = $A(arguments);
204
211
 
@@ -245,13 +252,17 @@ var Behavior = {
245
252
  return new this(element, Array.prototype.slice.call(arguments, 1));
246
253
  },
247
254
  _bindEvents : function(bound) {
248
- for (var member in bound)
249
- if (member.match(/^on(.+)/) && typeof bound[member] == 'function')
250
- bound.element.observe(RegExp.$1, Event.addBehavior._wrapObserver(bound[member].bindAsEventListener(bound)));
255
+ for (var member in bound) {
256
+ var matches = member.match(/^on(.+)/);
257
+ if (matches && typeof bound[member] == 'function')
258
+ bound.element.observe(matches[1], Event.addBehavior._wrapObserver(bound[member].bindAsEventListener(bound)));
259
+ }
251
260
  }
252
261
  }
253
262
  };
254
263
 
264
+
265
+
255
266
  Remote = Behavior.create({
256
267
  initialize: function(options) {
257
268
  if (this.element.nodeName == 'FORM') new Remote.Form(this.element, options);
@@ -264,11 +275,19 @@ Remote.Base = {
264
275
  this.options = Object.extend({
265
276
  evaluateScripts : true
266
277
  }, options || {});
278
+
279
+ this._bindCallbacks();
267
280
  },
268
281
  _makeRequest : function(options) {
269
282
  if (options.update) new Ajax.Updater(options.update, options.url, options);
270
283
  else new Ajax.Request(options.url, options);
271
284
  return false;
285
+ },
286
+ _bindCallbacks: function() {
287
+ $w('onCreate onComplete onException onFailure onInteractive onLoading onLoaded onSuccess').each(function(cb) {
288
+ if (Object.isFunction(this.options[cb]))
289
+ this.options[cb] = this.options[cb].bind(this);
290
+ }.bind(this));
272
291
  }
273
292
  }
274
293
 
@@ -1,7 +1,15 @@
1
1
  html, body {color: #193440; background: url(../images/300-ACD3E6-fff.png) repeat-x #fff; }
2
- .page-header {color: white; background: url(../images/100-3B5F87-ACD3E6.png) repeat-x #3F606E;}
3
- .page-header .navigation.main-nav a {background: #5B8BA0;}
4
- .page-header .navigation.main-nav li.current a {background: #FCFFF4; color: #222;}
2
+ .page-header {color: white; background: url(../images/101-3B5F87-ACD3E6.png) repeat-x #3F606E;}
3
+ .page-header .navigation.main-nav a {
4
+ background: url(../images/30-3E547A-242E42.png) repeat-x #242E42;
5
+ }
6
+ .page-header .navigation.main-nav li.current a {
7
+ color: #222;
8
+ background: url(../images/30-DBE1E5-FCFEF5.png) repeat-x #FCFEF5;
9
+ border-top: 1px solid white;
10
+ border-left: 1px solid white;
11
+ border-right: 1px solid white;
12
+ }
5
13
  .page-header .navigation.main-nav a:hover {background: #193440;}
6
14
  .section.content {background: #FCFFF5;}
7
15
  .button {color: white; background: #5B8BA0;}
@@ -77,7 +85,7 @@ form .actions input { margin: 0; }
77
85
  margin: 0 40px 10px; padding: 10px 30px; border-width: 2px 0;
78
86
  color: white;
79
87
  }
80
- .flash.notice {background: #92ab6e;}
88
+ .flash.notice {background: #4E6A8F;}
81
89
  .flash.error {background: #BC1C3D;}
82
90
  .section.with-flash { padding-top: 20px; }
83
91
 
@@ -39,7 +39,7 @@ class HoboSubsiteGenerator < Rails::Generator::NamedBase
39
39
  end
40
40
  puts "Renaming app/views/taglibs/application.dryml to app/views/taglibs/front_site.dryml"
41
41
  FileUtils.mv('app/views/taglibs/application.dryml', 'app/views/taglibs/front_site.dryml')
42
- m.template "application.dryml", File.join('app/views/taglibs/application.dryml')
42
+ m.template "application.dryml", File.join('app/views/taglibs/application.dryml')
43
43
  end
44
44
 
45
45
  m.directory File.join('app', 'controllers', file_name)
@@ -38,6 +38,28 @@ class HoboUserControllerGenerator < Rails::Generator::NamedBase
38
38
  m.template 'view.rhtml', path,
39
39
  :assigns => { :action => action, :path => path }
40
40
  end
41
+
42
+ if invite_only?
43
+ m.template "accept_invitation.dryml", File.join('app/views', class_path, file_name, "accept_invitation.dryml")
44
+ end
41
45
  end
42
46
  end
47
+
48
+ def invite_only?
49
+ options[:invite_only]
50
+ end
51
+
52
+ protected
53
+ def banner
54
+ "Usage: #{$0} #{spec.name} ModelName [--invite-only]"
55
+ end
56
+
57
+ def add_options!(opt)
58
+ opt.separator ''
59
+ opt.separator 'Options:'
60
+ opt.on("--invite-only", "Add features for an invite only website") do |v|
61
+ options[:invite_only] = true
62
+ end
63
+ end
64
+
43
65
  end
@@ -0,0 +1,5 @@
1
+ <accept-invitation-page>
2
+ <after-heading:>
3
+ <p>Welcome to <app-name/>, <name/>. To accept your invitation, please choose your password.</p>
4
+ </after-heading:>
5
+ </accept-invitation-page>
@@ -3,5 +3,27 @@ class <%= class_name %>Controller < ApplicationController
3
3
  hobo_user_controller
4
4
 
5
5
  auto_actions :all, :except => [ :index, :new, :create ]
6
+ <% if invite_only? -%>
7
+
8
+ def create
9
+ hobo_create do
10
+ if valid?
11
+ self.current_user = this
12
+ this.password = this.password_confirmation = nil # don't trigger password change validations
13
+ this.state = 'active'
14
+ this.save
15
+ flash[:notice] = "You are now the site administrator"
16
+ redirect_to home_page
17
+ end
18
+ end
19
+ end
20
+
21
+ def do_accept_invitation
22
+ do_transition_action :accept_invitation do
23
+ self.current_user = this
24
+ flash[:notice] = "You have signed up"
25
+ end
26
+ end
27
+ <% end -%>
6
28
 
7
29
  end
@@ -19,12 +19,28 @@ class HoboUserModelGenerator < Rails::Generator::NamedBase
19
19
 
20
20
  m.template 'mailer.rb', File.join('app/models', class_path, "#{file_name}_mailer.rb")
21
21
  m.template 'forgot_password.erb', File.join(mailer_dir, "forgot_password.erb")
22
+
23
+ if invite_only?
24
+ m.template 'invite.erb', File.join(mailer_dir, "invite.erb")
25
+ end
22
26
  end
23
27
  end
28
+
29
+ def invite_only?
30
+ options[:invite_only]
31
+ end
24
32
 
25
33
  protected
26
34
  def banner
27
- "Usage: #{$0} #{spec.name} ModelName"
35
+ "Usage: #{$0} #{spec.name} ModelName [--invite-only]"
36
+ end
37
+
38
+ def add_options!(opt)
39
+ opt.separator ''
40
+ opt.separator 'Options:'
41
+ opt.on("--invite-only", "Add features for an invite only website") do |v|
42
+ options[:invite_only] = true
43
+ end
28
44
  end
29
45
 
30
46
  end
@@ -0,0 +1,9 @@
1
+ <%%= @user %>,
2
+
3
+ You have been invited to join <%%= @app_name %>. If you wish to accept, please click on the following link
4
+
5
+ <%%= user_accept_invitation_url :host => @host, :id => @user, :key => @key %>
6
+
7
+ Thank you,
8
+
9
+ The <%%= @app_name %> team.
@@ -10,5 +10,20 @@ class <%= class_name -%>Mailer < ActionMailer::Base
10
10
  @sent_on = Time.now
11
11
  @headers = {}
12
12
  end
13
+ <% if invite_only? -%>
13
14
 
15
+ def invite(user, key)
16
+ host = Hobo::Controller.request_host
17
+ app_name = Hobo::Controller.app_name || host
18
+ # FIXME - nasty hack
19
+ app_name.remove!(/ - Admin$/)
20
+ @subject = "Invitation to #{app_name}"
21
+ @body = { :user => user, :key => key, :host => host, :app_name => app_name }
22
+ @recipients = user.email_address
23
+ @from = "no-reply@#{host}"
24
+ @sent_on = Time.now
25
+ @headers = {}
26
+ end
27
+ <% end -%>
28
+
14
29
  end
@@ -11,19 +11,39 @@ class <%= class_name %> < ActiveRecord::Base
11
11
 
12
12
  # This gives admin rights to the first sign-up.
13
13
  # Just remove it if you don't want that
14
- before_create { |user| user.administrator = true if RAILS_ENV != "test" && count == 0 }
15
-
14
+ before_create { |user| user.administrator = true if !Rails.env.test? && count == 0 }
15
+
16
+ <% if invite_only? -%>
17
+ validates_confirmation_of :password, :if => "User.count == 0"
18
+ <% end -%>
16
19
 
17
20
  # --- Signup lifecycle --- #
18
21
 
19
22
  lifecycle do
20
23
 
24
+ <% if invite_only? -%>
25
+ state :invited, :default => true
26
+ state :active
27
+
28
+ create :invite,
29
+ :available_to => "acting_user if acting_user.administrator?",
30
+ :params => [:name, :email_address],
31
+ :new_key => true,
32
+ :become => :invited do
33
+ UserMailer.deliver_invite(self, lifecycle.key)
34
+ end
35
+
36
+ transition :accept_invitation, { :invited => :active }, :available_to => :key_holder,
37
+ :params => [ :password, :password_confirmation ]
38
+
39
+ <% else -%>
21
40
  state :active, :default => true
22
41
 
23
42
  create :signup, :available_to => "Guest",
24
43
  :params => [:name, :email_address, :password, :password_confirmation],
25
44
  :become => :active
26
-
45
+
46
+ <% end -%>
27
47
  transition :request_password_reset, { :active => :active }, :new_key => true do
28
48
  <%= class_name %>Mailer.deliver_forgot_password(self, lifecycle.key)
29
49
  end
@@ -37,11 +57,18 @@ class <%= class_name %> < ActiveRecord::Base
37
57
  # --- Permissions --- #
38
58
 
39
59
  def create_permitted?
60
+ <% if invite_only? -%>
61
+ # Only the initial admin user can be created, from there it's invite-only
62
+ User.count == 0
63
+ <% else -%>
40
64
  false
65
+ <% end -%>
41
66
  end
42
67
 
43
68
  def update_permitted?
44
- acting_user.administrator? || (acting_user == self && only_changed?(:crypted_password, :email_address))
69
+ acting_user.administrator? ||
70
+ (acting_user == self && only_changed?(:email_address, :crypted_password,
71
+ :current_password, :password, :password_confirmation))
45
72
  # Note: crypted_password has attr_protected so although it is permitted to change, it cannot be changed
46
73
  # directly from a form submission.
47
74
  end