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
@@ -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