hobo 0.8.8 → 0.8.9
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGES.txt +34 -0
- data/Rakefile +30 -24
- data/bin/hobo +30 -10
- data/doctest/hobo/hobo_helper.rdoctest +92 -0
- data/doctest/hobo/lifecycles.rdoctest +261 -0
- data/doctest/scopes.rdoctest +387 -0
- data/dryml_generators/rapid/forms.dryml.erb +3 -3
- data/dryml_generators/rapid/pages.dryml.erb +4 -4
- data/lib/active_record/viewhints_validations_interceptor.rb +1 -1
- data/lib/hobo.rb +1 -1
- data/lib/hobo/accessible_associations.rb +3 -3
- data/lib/hobo/authentication_support.rb +1 -1
- data/lib/hobo/dryml.rb +10 -0
- data/lib/hobo/dryml/taglib.rb +3 -5
- data/lib/hobo/hobo_helper.rb +3 -1
- data/lib/hobo/include_in_save.rb +1 -0
- data/lib/hobo/lifecycles/actions.rb +6 -2
- data/lib/hobo/model.rb +1 -1
- data/lib/hobo/model_controller.rb +34 -12
- data/lib/hobo/permissions.rb +1 -1
- data/lib/hobo/rapid_helper.rb +3 -0
- data/lib/hobo/scopes/association_proxy_extensions.rb +8 -2
- data/lib/hobo/scopes/automatic_scopes.rb +3 -3
- data/lib/hobo/user_controller.rb +2 -1
- data/rails_generators/hobo/hobo_generator.rb +1 -1
- data/rails_generators/hobo/templates/application.dryml +0 -2
- data/rails_generators/hobo_admin_site/hobo_admin_site_generator.rb +45 -0
- data/rails_generators/hobo_admin_site/templates/admin.css +2 -0
- data/rails_generators/hobo_admin_site/templates/application.dryml +1 -0
- data/rails_generators/hobo_admin_site/templates/controller.rb +13 -0
- data/rails_generators/hobo_admin_site/templates/site_taglib.dryml +32 -0
- data/rails_generators/hobo_admin_site/templates/users_index.dryml +5 -0
- data/rails_generators/hobo_front_controller/hobo_front_controller_generator.rb +7 -1
- data/rails_generators/hobo_front_controller/templates/index.dryml +16 -0
- data/rails_generators/hobo_rapid/hobo_rapid_generator.rb +31 -1
- data/rails_generators/hobo_rapid/templates/hobo-rapid.js +5 -3
- data/rails_generators/hobo_rapid/templates/lowpro.js +40 -21
- data/rails_generators/hobo_rapid/templates/themes/clean/public/images/101-3B5F87-ACD3E6.png +0 -0
- data/rails_generators/hobo_rapid/templates/themes/clean/public/images/30-3E547A-242E42.png +0 -0
- data/rails_generators/hobo_rapid/templates/themes/clean/public/images/30-DBE1E5-FCFEF5.png +0 -0
- data/rails_generators/hobo_rapid/templates/themes/clean/public/stylesheets/clean.css +12 -4
- data/rails_generators/hobo_subsite/hobo_subsite_generator.rb +1 -1
- data/rails_generators/hobo_user_controller/hobo_user_controller_generator.rb +22 -0
- data/rails_generators/hobo_user_controller/templates/accept_invitation.dryml +5 -0
- data/rails_generators/hobo_user_controller/templates/controller.rb +22 -0
- data/rails_generators/hobo_user_model/hobo_user_model_generator.rb +17 -1
- data/rails_generators/hobo_user_model/templates/invite.erb +9 -0
- data/rails_generators/hobo_user_model/templates/mailer.rb +15 -0
- data/rails_generators/hobo_user_model/templates/model.rb +31 -4
- data/taglibs/rapid_core.dryml +25 -6
- data/taglibs/rapid_forms.dryml +65 -24
- data/taglibs/rapid_lifecycles.dryml +1 -1
- data/taglibs/rapid_navigation.dryml +2 -2
- data/taglibs/rapid_plus.dryml +4 -3
- metadata +151 -210
- data/Manifest +0 -155
- data/hobo.gemspec +0 -46
- 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="¤t_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
|
-
|
632
|
-
|
633
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
126
|
-
$(element).observe(event,
|
127
|
-
Event.addBehavior.cache.push([element, event,
|
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
|
-
|
250
|
-
|
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
|
|
Binary file
|
Binary file
|
Binary file
|
@@ -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/
|
3
|
-
.page-header .navigation.main-nav a {
|
4
|
-
|
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: #
|
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
|
@@ -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
|
@@ -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
|
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? ||
|
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
|