bmarzolf-picnic 0.8.0.20090420
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.
- data/CHANGELOG.txt +1 -0
- data/History.txt +78 -0
- data/LICENSE.txt +165 -0
- data/Manifest.txt +45 -0
- data/README.txt +31 -0
- data/Rakefile +64 -0
- data/lib/picnic/authentication.rb +254 -0
- data/lib/picnic/cli.rb +165 -0
- data/lib/picnic/conf.rb +135 -0
- data/lib/picnic/controllers.rb +4 -0
- data/lib/picnic/logger.rb +41 -0
- data/lib/picnic/server.rb +99 -0
- data/lib/picnic/service_control.rb +274 -0
- data/lib/picnic/version.rb +9 -0
- data/lib/picnic.rb +11 -0
- data/setup.rb +1585 -0
- data/test/picnic_test.rb +11 -0
- data/test/test_helper.rb +2 -0
- data/vendor/camping-2.0.20090420/CHANGELOG +118 -0
- data/vendor/camping-2.0.20090420/COPYING +18 -0
- data/vendor/camping-2.0.20090420/README +82 -0
- data/vendor/camping-2.0.20090420/Rakefile +180 -0
- data/vendor/camping-2.0.20090420/bin/camping +97 -0
- data/vendor/camping-2.0.20090420/doc/camping.1.gz +0 -0
- data/vendor/camping-2.0.20090420/examples/README +5 -0
- data/vendor/camping-2.0.20090420/examples/blog.rb +375 -0
- data/vendor/camping-2.0.20090420/examples/campsh.rb +629 -0
- data/vendor/camping-2.0.20090420/examples/tepee.rb +242 -0
- data/vendor/camping-2.0.20090420/extras/Camping.gif +0 -0
- data/vendor/camping-2.0.20090420/extras/permalink.gif +0 -0
- data/vendor/camping-2.0.20090420/lib/camping/ar/session.rb +132 -0
- data/vendor/camping-2.0.20090420/lib/camping/ar.rb +78 -0
- data/vendor/camping-2.0.20090420/lib/camping/mab.rb +26 -0
- data/vendor/camping-2.0.20090420/lib/camping/reloader.rb +184 -0
- data/vendor/camping-2.0.20090420/lib/camping/server.rb +159 -0
- data/vendor/camping-2.0.20090420/lib/camping/session.rb +75 -0
- data/vendor/camping-2.0.20090420/lib/camping-unabridged.rb +630 -0
- data/vendor/camping-2.0.20090420/lib/camping.rb +52 -0
- data/vendor/camping-2.0.20090420/setup.rb +1551 -0
- data/vendor/camping-2.0.20090420/test/apps/env_debug.rb +65 -0
- data/vendor/camping-2.0.20090420/test/apps/forms.rb +95 -0
- data/vendor/camping-2.0.20090420/test/apps/misc.rb +86 -0
- data/vendor/camping-2.0.20090420/test/apps/sessions.rb +38 -0
- data/vendor/camping-2.0.20090420/test/test_camping.rb +54 -0
- metadata +140 -0
@@ -0,0 +1,242 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
$:.unshift File.dirname(__FILE__) + "/../lib"
|
3
|
+
%w(rubygems redcloth camping camping/ar acts_as_versioned).each { |lib| require lib }
|
4
|
+
|
5
|
+
Camping.goes :Tepee
|
6
|
+
|
7
|
+
module Tepee::Models
|
8
|
+
|
9
|
+
class Page < Base
|
10
|
+
PAGE_LINK = /\[\[([^\]|]*)[|]?([^\]]*)\]\]/
|
11
|
+
validates_uniqueness_of :title
|
12
|
+
before_save { |r| r.title = r.title.underscore }
|
13
|
+
acts_as_versioned
|
14
|
+
end
|
15
|
+
|
16
|
+
class CreateTepee < V 1.0
|
17
|
+
def self.up
|
18
|
+
create_table :tepee_pages do |t|
|
19
|
+
t.column :title, :string, :limit => 255
|
20
|
+
t.column :body, :text
|
21
|
+
end
|
22
|
+
Page.create_versioned_table
|
23
|
+
Page.reset_column_information
|
24
|
+
end
|
25
|
+
def self.down
|
26
|
+
drop_table :tepee_pages
|
27
|
+
Page.drop_versioned_table
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module Tepee::Controllers
|
34
|
+
class Index < R '/'
|
35
|
+
def get
|
36
|
+
redirect Show, 'home'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Show < R '/(\w+)', '/(\w+)/(\d+)'
|
41
|
+
def get page_name, version = nil
|
42
|
+
redirect(Edit, page_name, 1) and return unless @page = Page.find_by_title(page_name)
|
43
|
+
@version = (version.nil? or version == @page.version.to_s) ? @page : @page.versions.find_by_version(version)
|
44
|
+
render :show
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class Edit < R '/(\w+)/edit', '/(\w+)/(\d+)/edit'
|
49
|
+
def get page_name, version = nil
|
50
|
+
@page = Page.find_or_create_by_title(page_name)
|
51
|
+
@page = @page.versions.find_by_version(version) unless version.nil? or version == @page.version.to_s
|
52
|
+
render :edit
|
53
|
+
end
|
54
|
+
|
55
|
+
def post page_name
|
56
|
+
Page.find_or_create_by_title(page_name).update_attributes :body => input.post_body and redirect Show, page_name
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Versions < R '/(\w+)/versions'
|
61
|
+
def get page_name
|
62
|
+
@page = Page.find_or_create_by_title(page_name)
|
63
|
+
@versions = @page.versions
|
64
|
+
render :versions
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class List < R '/all/list'
|
69
|
+
def get
|
70
|
+
@pages = Page.find :all, :order => 'title'
|
71
|
+
render :list
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class Stylesheet < R '/css/tepee.css'
|
76
|
+
def get
|
77
|
+
@headers['Content-Type'] = 'text/css'
|
78
|
+
File.read(__FILE__).gsub(/.*__END__/m, '')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module Tepee::Views
|
84
|
+
def layout
|
85
|
+
html do
|
86
|
+
head do
|
87
|
+
title 'test'
|
88
|
+
link :href=>R(Stylesheet), :rel=>'stylesheet', :type=>'text/css'
|
89
|
+
end
|
90
|
+
style <<-END, :type => 'text/css'
|
91
|
+
body {
|
92
|
+
font-family: verdana, arial, sans-serif;
|
93
|
+
}
|
94
|
+
h1, h2, h3, h4, h5 {
|
95
|
+
font-weight: normal;
|
96
|
+
}
|
97
|
+
p.actions a {
|
98
|
+
margin-right: 6px;
|
99
|
+
}
|
100
|
+
END
|
101
|
+
body do
|
102
|
+
p do
|
103
|
+
small do
|
104
|
+
span "welcome to " ; a 'tepee', :href => "http://code.whytheluckystiff.net/svn/camping/trunk/examples/tepee.rb"
|
105
|
+
span '. go ' ; a 'home', :href => R(Show, 'home')
|
106
|
+
span '. list all ' ; a 'pages', :href => R(List)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
div.content do
|
110
|
+
self << yield
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def show
|
117
|
+
h1 @page.title
|
118
|
+
div { _markup @version.body }
|
119
|
+
p.actions do
|
120
|
+
_button 'edit', :href => R(Edit, @version.title, @version.version)
|
121
|
+
_button 'back', :href => R(Show, @version.title, @version.version-1) unless @version.version == 1
|
122
|
+
_button 'next', :href => R(Show, @version.title, @version.version+1) unless @version.version == @page.version
|
123
|
+
_button 'current', :href => R(Show, @version.title) unless @version.version == @page.version
|
124
|
+
_button 'versions', :href => R(Versions, @page.title)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def edit
|
129
|
+
h1 @page.title
|
130
|
+
form :method => 'post', :action => R(Edit, @page.title) do
|
131
|
+
p do
|
132
|
+
textarea @page.body, :name => 'post_body', :rows => 50, :cols => 100
|
133
|
+
end
|
134
|
+
input :type => 'submit', :value=>'change'
|
135
|
+
end
|
136
|
+
_button 'cancel', :href => R(Show, @page.title, @page.version)
|
137
|
+
a 'syntax', :href => 'http://hobix.com/textile/', :target=>'_blank'
|
138
|
+
end
|
139
|
+
|
140
|
+
def list
|
141
|
+
h1 'all pages'
|
142
|
+
ul { @pages.each { |p| li { a p.title, :href => R(Show, p.title) } } }
|
143
|
+
end
|
144
|
+
|
145
|
+
def versions
|
146
|
+
h1 @page.title
|
147
|
+
ul do
|
148
|
+
@versions.each do |page|
|
149
|
+
li do
|
150
|
+
span page.version
|
151
|
+
_button 'show', :href => R(Show, page.title, page.version)
|
152
|
+
_button 'edit', :href => R(Edit, page.title, page.version)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def _button(text, options={})
|
159
|
+
form :method=>:get, :action=>options[:href] do
|
160
|
+
input :type=>'submit', :name=>'submit', :value=>text
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def _markup body
|
165
|
+
return '' if body.blank?
|
166
|
+
body.gsub!(Tepee::Models::Page::PAGE_LINK) do
|
167
|
+
page = title = $1
|
168
|
+
title = $2 unless $2.empty?
|
169
|
+
page = page.gsub /\W/, '_'
|
170
|
+
if Tepee::Models::Page.find(:all, :select => 'title').collect { |p| p.title }.include?(page)
|
171
|
+
%Q{<a href="#{self/R(Show, page)}">#{title}</a>}
|
172
|
+
else
|
173
|
+
%Q{<span>#{title}<a href="#{self/R(Edit, page, 1)}">?</a></span>}
|
174
|
+
end
|
175
|
+
end
|
176
|
+
RedCloth.new(body, [ :hard_breaks ]).to_html
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def Tepee.create
|
181
|
+
Tepee::Models.create_schema :assume => (Tepee::Models::Page.table_exists? ? 1.0 : 0.0)
|
182
|
+
end
|
183
|
+
__END__
|
184
|
+
/** focus **/
|
185
|
+
/*
|
186
|
+
a:hover:active {
|
187
|
+
color: #10bae0;
|
188
|
+
}
|
189
|
+
|
190
|
+
a:not(:hover):active {
|
191
|
+
color: #0000ff;
|
192
|
+
}
|
193
|
+
|
194
|
+
*:focus {
|
195
|
+
-moz-outline: 2px solid #10bae0 !important;
|
196
|
+
-moz-outline-offset: 1px !important;
|
197
|
+
-moz-outline-radius: 3px !important;
|
198
|
+
}
|
199
|
+
|
200
|
+
button:focus,
|
201
|
+
input[type="reset"]:focus,
|
202
|
+
input[type="button"]:focus,
|
203
|
+
input[type="submit"]:focus,
|
204
|
+
input[type="file"] > input[type="button"]:focus {
|
205
|
+
-moz-outline-radius: 5px !important;
|
206
|
+
}
|
207
|
+
|
208
|
+
button:focus::-moz-focus-inner {
|
209
|
+
border-color: transparent !important;
|
210
|
+
}
|
211
|
+
|
212
|
+
button::-moz-focus-inner,
|
213
|
+
input[type="reset"]::-moz-focus-inner,
|
214
|
+
input[type="button"]::-moz-focus-inner,
|
215
|
+
input[type="submit"]::-moz-focus-inner,
|
216
|
+
input[type="file"] > input[type="button"]::-moz-focus-inner {
|
217
|
+
border: 1px dotted transparent !important;
|
218
|
+
}
|
219
|
+
textarea:focus, button:focus, select:focus, input:focus {
|
220
|
+
-moz-outline-offset: -1px !important;
|
221
|
+
}
|
222
|
+
input[type="radio"]:focus {
|
223
|
+
-moz-outline-radius: 12px;
|
224
|
+
-moz-outline-offset: 0px !important;
|
225
|
+
}
|
226
|
+
a:focus {
|
227
|
+
-moz-outline-offset: 0px !important;
|
228
|
+
}
|
229
|
+
*/
|
230
|
+
form { display: inline; }
|
231
|
+
|
232
|
+
/** Gradient **/
|
233
|
+
small, pre, textarea, textfield, button, input, select {
|
234
|
+
color: #4B4B4C !important;
|
235
|
+
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAAeCAMAAAAxfD/2AAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAAtUExURfT09PLy8vHx8fv7+/j4+PX19fn5+fr6+vf39/z8/Pb29vPz8/39/f7+/v///0c8Y4oAAAA5SURBVHjaXMZJDgAgCMDAuouA/3+uHPRiMmlKzmhCFRorLOakVnpnDEpBBDHM8ODs/bz372+PAAMAXIQCfD6uIDsAAAAASUVORK5CYII=) !important;
|
236
|
+
background-color: #FFF !important;
|
237
|
+
background-repeat: repeat-x !important;
|
238
|
+
border: 1px solid #CCC !important;
|
239
|
+
}
|
240
|
+
|
241
|
+
button, input { margin: 3px; }
|
242
|
+
|
Binary file
|
Binary file
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# == About camping/ar/session.rb
|
2
|
+
#
|
3
|
+
# This file contains two modules which supply basic sessioning to your Camping app.
|
4
|
+
# Again, we're dealing with a pretty little bit of code: approx. 60 lines.
|
5
|
+
#
|
6
|
+
# * Camping::Models::Session is a module which adds a single <tt>sessions</tt> table
|
7
|
+
# to your database.
|
8
|
+
# * Camping::ARSession is a module which you will mix into your application (or into
|
9
|
+
# specific controllers which require sessions) to supply a <tt>@state</tt> variable
|
10
|
+
# you can use in controllers and views.
|
11
|
+
#
|
12
|
+
# For a basic tutorial, see the *Getting Started* section of the Camping::ARSession module.
|
13
|
+
require 'camping'
|
14
|
+
require 'camping/ar'
|
15
|
+
|
16
|
+
module Camping::Models
|
17
|
+
# A database table for storing Camping sessions. Contains a unique 32-character hashid, a
|
18
|
+
# creation timestamp, and a column of serialized data called <tt>ivars</tt>.
|
19
|
+
class Session < Base
|
20
|
+
serialize :ivars
|
21
|
+
set_primary_key :hashid
|
22
|
+
|
23
|
+
def []=(k, v) # :nodoc:
|
24
|
+
self.ivars[k] = v
|
25
|
+
end
|
26
|
+
def [](k) # :nodoc:
|
27
|
+
self.ivars[k] rescue nil
|
28
|
+
end
|
29
|
+
|
30
|
+
protected
|
31
|
+
RAND_CHARS = [*'A'..'Z'] + [*'0'..'9'] + [*'a'..'z']
|
32
|
+
def before_create
|
33
|
+
rand_max = RAND_CHARS.size
|
34
|
+
sid = (0...32).inject("") { |ret,_| ret << RAND_CHARS[rand(rand_max)] }
|
35
|
+
write_attribute('hashid', sid)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Generates a new session ID and creates a row for the new session in the database.
|
39
|
+
def self.generate cookies
|
40
|
+
sess = Session.create :ivars => Camping::H[]
|
41
|
+
cookies.camping_sid = sess.hashid
|
42
|
+
sess
|
43
|
+
end
|
44
|
+
|
45
|
+
# Gets the existing session based on the <tt>camping_sid</tt> available in cookies.
|
46
|
+
# If none is found, generates a new session.
|
47
|
+
def self.persist cookies
|
48
|
+
session = nil
|
49
|
+
if cookies.camping_sid
|
50
|
+
session = Camping::Models::Session.find_by_hashid cookies.camping_sid
|
51
|
+
end
|
52
|
+
unless session
|
53
|
+
session = Camping::Models::Session.generate cookies
|
54
|
+
end
|
55
|
+
session
|
56
|
+
end
|
57
|
+
|
58
|
+
# Builds the session table in the database. To be used in your application's
|
59
|
+
# <tt>create</tt> method.
|
60
|
+
#
|
61
|
+
# Like so:
|
62
|
+
#
|
63
|
+
# def Blog.create
|
64
|
+
# Camping::Models::Session.create_schema
|
65
|
+
# unless Blog::Models::Post.table_exists?
|
66
|
+
# ActiveRecord::Schema.define(&Blog::Models.schema)
|
67
|
+
# end
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
def self.create_schema
|
71
|
+
unless table_exists?
|
72
|
+
ActiveRecord::Schema.define do
|
73
|
+
create_table :sessions, :force => true, :id => false do |t|
|
74
|
+
t.column :hashid, :string, :limit => 32, :null => false
|
75
|
+
t.column :created_at, :datetime
|
76
|
+
t.column :ivars, :text
|
77
|
+
end
|
78
|
+
add_index :sessions, [:hashid], :unique => true
|
79
|
+
end
|
80
|
+
reset_column_information
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
Session.partial_updates = false if Session.respond_to?(:partial_updates=)
|
85
|
+
end
|
86
|
+
|
87
|
+
module Camping
|
88
|
+
# The Camping::ARSession module is designed to be mixed into your application or into specific
|
89
|
+
# controllers which require sessions. This module defines a <tt>service</tt> method which
|
90
|
+
# intercepts all requests handed to those controllers.
|
91
|
+
#
|
92
|
+
# == Getting Started
|
93
|
+
#
|
94
|
+
# To get sessions working for your application:
|
95
|
+
#
|
96
|
+
# 1. <tt>require 'camping/session'</tt>
|
97
|
+
# 2. Mixin the module: <tt>module YourApp; include Camping::ARSession end</tt>
|
98
|
+
# 3. In your application's <tt>create</tt> method, add a call to <tt>Camping::Models::Session.create_schema</tt>
|
99
|
+
# 4. Throughout your application, use the <tt>@state</tt> var like a hash to store your application's data.
|
100
|
+
#
|
101
|
+
# If you are unfamiliar with the <tt>create</tt> method, see
|
102
|
+
# http://code.whytheluckystiff.net/camping/wiki/GiveUsTheCreateMethod.
|
103
|
+
#
|
104
|
+
# == A Few Notes
|
105
|
+
#
|
106
|
+
# * The session ID is stored in a cookie. Look in <tt>@cookies.camping_sid</tt>.
|
107
|
+
# * The session data is stored in the <tt>sessions</tt> table in your database.
|
108
|
+
# * All mounted Camping apps using this class will use the same database table.
|
109
|
+
# * However, your application's data is stored in its own hash.
|
110
|
+
# * Session data is only saved if it has changed.
|
111
|
+
module ARSession
|
112
|
+
# This <tt>service</tt> method, when mixed into controllers, intercepts requests
|
113
|
+
# and wraps them with code to start and close the session. If a session isn't found
|
114
|
+
# in the database it is created. The <tt>@state</tt> variable is set and if it changes,
|
115
|
+
# it is saved back into the database.
|
116
|
+
def service(*a)
|
117
|
+
session = Camping::Models::Session.persist @cookies
|
118
|
+
app = self.class.name.gsub(/^(\w+)::.+$/, '\1')
|
119
|
+
@state = (session[app] ||= Camping::H[])
|
120
|
+
hash_before = Marshal.dump(@state).hash
|
121
|
+
return super(*a)
|
122
|
+
ensure
|
123
|
+
if session
|
124
|
+
hash_after = Marshal.dump(@state).hash
|
125
|
+
unless hash_before == hash_after
|
126
|
+
session[app] = @state
|
127
|
+
session.save
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class MissingLibrary < Exception #:nodoc: all
|
2
|
+
end
|
3
|
+
begin
|
4
|
+
require 'active_record'
|
5
|
+
rescue LoadError => e
|
6
|
+
raise MissingLibrary, "ActiveRecord could not be loaded (is it installed?): #{e.message}"
|
7
|
+
end
|
8
|
+
|
9
|
+
$AR_EXTRAS = %{
|
10
|
+
Base = ActiveRecord::Base unless const_defined? :Base
|
11
|
+
|
12
|
+
def Y; ActiveRecord::Base.verify_active_connections!; self; end
|
13
|
+
|
14
|
+
class SchemaInfo < Base
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.V(n)
|
18
|
+
@final = [n, @final.to_i].max
|
19
|
+
m = (@migrations ||= [])
|
20
|
+
Class.new(ActiveRecord::Migration) do
|
21
|
+
meta_def(:version) { n }
|
22
|
+
meta_def(:inherited) { |k| m << k }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.create_schema(opts = {})
|
27
|
+
opts[:assume] ||= 0
|
28
|
+
opts[:version] ||= @final
|
29
|
+
if @migrations
|
30
|
+
unless SchemaInfo.table_exists?
|
31
|
+
ActiveRecord::Schema.define do
|
32
|
+
create_table SchemaInfo.table_name do |t|
|
33
|
+
t.column :version, :float
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
si = SchemaInfo.find(:first) || SchemaInfo.new(:version => opts[:assume])
|
39
|
+
if si.version < opts[:version]
|
40
|
+
@migrations.each do |k|
|
41
|
+
k.migrate(:up) if si.version < k.version and k.version <= opts[:version]
|
42
|
+
k.migrate(:down) if si.version > k.version and k.version > opts[:version]
|
43
|
+
end
|
44
|
+
si.update_attributes(:version => opts[:version])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
}
|
49
|
+
|
50
|
+
module Camping
|
51
|
+
module Models
|
52
|
+
A = ActiveRecord
|
53
|
+
# Base is an alias for ActiveRecord::Base. The big warning I'm going to give you
|
54
|
+
# about this: *Base overloads table_name_prefix.* This means that if you have a
|
55
|
+
# model class Blog::Models::Post, it's table name will be <tt>blog_posts</tt>.
|
56
|
+
#
|
57
|
+
# ActiveRecord is not loaded if you never reference this class. The minute you
|
58
|
+
# use the ActiveRecord or Camping::Models::Base class, then the ActiveRecord library
|
59
|
+
# is loaded.
|
60
|
+
Base = A::Base
|
61
|
+
|
62
|
+
# The default prefix for Camping model classes is the topmost module name lowercase
|
63
|
+
# and followed with an underscore.
|
64
|
+
#
|
65
|
+
# Tepee::Models::Page.table_name_prefix
|
66
|
+
# #=> "tepee_pages"
|
67
|
+
#
|
68
|
+
def Base.table_name_prefix
|
69
|
+
"#{name[/\w+/]}_".downcase.sub(/^(#{A}|camping)_/i,'')
|
70
|
+
end
|
71
|
+
module_eval $AR_EXTRAS
|
72
|
+
end
|
73
|
+
end
|
74
|
+
Camping::S.sub! /autoload\s*:Base\s*,\s*['"]camping\/ar['"]/, ""
|
75
|
+
Camping::S.sub! /def\s*Y[;\s]*self[;\s]*end/, $AR_EXTRAS
|
76
|
+
Camping::Apps.each do |c|
|
77
|
+
c::Models.module_eval $AR_EXTRAS
|
78
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class MissingLibrary < Exception #:nodoc: all
|
2
|
+
end
|
3
|
+
begin
|
4
|
+
require 'markaby'
|
5
|
+
rescue LoadError => e
|
6
|
+
raise MissingLibrary, "Markaby could not be loaded (is it installed?): #{e.message}"
|
7
|
+
end
|
8
|
+
|
9
|
+
$MAB_CODE = %{
|
10
|
+
# The Mab class wraps Markaby, allowing it to run methods from Camping::Views
|
11
|
+
# and also to replace :href, :action and :src attributes in tags by prefixing the root
|
12
|
+
# path.
|
13
|
+
class Mab < Markaby::Builder
|
14
|
+
include Views
|
15
|
+
def tag!(*g,&b)
|
16
|
+
h=g[-1]
|
17
|
+
[:href,:action,:src].map{|a|(h[a]&&=self/h[a])rescue 0}
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
}
|
22
|
+
|
23
|
+
Camping::S.sub! /autoload\s*:Mab\s*,\s*['"]camping\/mab['"]/, $MAB_CODE
|
24
|
+
Camping::Apps.each do |c|
|
25
|
+
c.module_eval $MAB_CODE
|
26
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
module Camping
|
2
|
+
# == The Camping Reloader
|
3
|
+
#
|
4
|
+
# Camping apps are generally small and predictable. Many Camping apps are
|
5
|
+
# contained within a single file. Larger apps are split into a handful of
|
6
|
+
# other Ruby libraries within the same directory.
|
7
|
+
#
|
8
|
+
# Since Camping apps (and their dependencies) are loaded with Ruby's require
|
9
|
+
# method, there is a record of them in $LOADED_FEATURES. Which leaves a
|
10
|
+
# perfect space for this class to manage auto-reloading an app if any of its
|
11
|
+
# immediate dependencies changes.
|
12
|
+
#
|
13
|
+
# == Wrapping Your Apps
|
14
|
+
#
|
15
|
+
# Since bin/camping and the Camping::Server class already use the Reloader,
|
16
|
+
# you probably don't need to hack it on your own. But, if you're rolling your
|
17
|
+
# own situation, here's how.
|
18
|
+
#
|
19
|
+
# Rather than this:
|
20
|
+
#
|
21
|
+
# require 'yourapp'
|
22
|
+
#
|
23
|
+
# Use this:
|
24
|
+
#
|
25
|
+
# require 'camping/reloader'
|
26
|
+
# reloader = Camping::Reloader.new('/path/to/yourapp.rb')
|
27
|
+
# blog = reloader.apps[:Blog]
|
28
|
+
# wiki = reloader.apps[:Wiki]
|
29
|
+
#
|
30
|
+
# The <tt>blog</tt> and <tt>wiki</tt> objects will behave exactly like your
|
31
|
+
# Blog and Wiki, but they will update themselves if yourapp.rb changes.
|
32
|
+
#
|
33
|
+
# You can also give Reloader more than one script.
|
34
|
+
class Reloader
|
35
|
+
attr_reader :scripts
|
36
|
+
|
37
|
+
# This is a simple wrapper which causes the script to reload (if needed)
|
38
|
+
# on any method call. Then the method call will be forwarded to the
|
39
|
+
# app.
|
40
|
+
class App # :nodoc:
|
41
|
+
instance_methods.each { |m| undef_method m unless m =~ /^__/ }
|
42
|
+
attr_accessor :app, :script
|
43
|
+
|
44
|
+
def initialize(script)
|
45
|
+
@script = script
|
46
|
+
end
|
47
|
+
|
48
|
+
# Reloads if needed, before calling the method on the app.
|
49
|
+
def method_missing(meth, *args, &blk)
|
50
|
+
@script.reload!
|
51
|
+
@app.send(meth, *args, &blk)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# This class is doing all the hard work; however, it only works on
|
56
|
+
# single files. Reloader just wraps up support for multiple scripts
|
57
|
+
# and hides away some methods you normally won't need.
|
58
|
+
class Script # :nodoc:
|
59
|
+
attr_reader :apps, :file, :dir, :extras
|
60
|
+
|
61
|
+
def initialize(file)
|
62
|
+
@file = File.expand_path(file)
|
63
|
+
@dir = File.dirname(@file)
|
64
|
+
@extras = File.join(@dir, File.basename(@file, ".rb"))
|
65
|
+
@mtime = Time.at(0)
|
66
|
+
@requires = []
|
67
|
+
@apps = {}
|
68
|
+
end
|
69
|
+
|
70
|
+
# Loads the apps availble in this script. Use <tt>apps</tt> to get
|
71
|
+
# the loaded apps.
|
72
|
+
def load_apps
|
73
|
+
all_requires = $LOADED_FEATURES.dup
|
74
|
+
all_apps = Camping::Apps.dup
|
75
|
+
|
76
|
+
begin
|
77
|
+
load(@file)
|
78
|
+
rescue Exception => e
|
79
|
+
puts "!! Error loading #{@file}:"
|
80
|
+
puts "#{e.class}: #{e.message}"
|
81
|
+
puts e.backtrace
|
82
|
+
puts "!! Error loading #{@file}, see backtrace above"
|
83
|
+
end
|
84
|
+
|
85
|
+
@requires = ($LOADED_FEATURES - all_requires).map do |req|
|
86
|
+
full = full_path(req)
|
87
|
+
full if full == @file or full.index(@extras) == 0
|
88
|
+
end
|
89
|
+
|
90
|
+
@mtime = mtime
|
91
|
+
|
92
|
+
new_apps = (Camping::Apps - all_apps)
|
93
|
+
old_apps = @apps.dup
|
94
|
+
@apps = new_apps.inject({}) do |hash, app|
|
95
|
+
key = app.name.to_sym
|
96
|
+
hash[key] = (old = old_apps[key]) || App.new(self)
|
97
|
+
hash[key].app = app
|
98
|
+
app.create if app.respond_to?(:create) && !old
|
99
|
+
hash
|
100
|
+
end
|
101
|
+
self
|
102
|
+
end
|
103
|
+
|
104
|
+
# Removes all the apps defined in this script.
|
105
|
+
def remove_apps
|
106
|
+
@apps.each do |name, app|
|
107
|
+
Camping::Apps.delete(app.app)
|
108
|
+
Object.send :remove_const, name
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Reloads the file if needed. No harm is done by calling this multiple
|
113
|
+
# times, so feel free call just to be sure.
|
114
|
+
def reload!
|
115
|
+
return if @mtime >= mtime
|
116
|
+
remove_apps
|
117
|
+
load_apps
|
118
|
+
end
|
119
|
+
|
120
|
+
# Checks if both scripts watches the same file.
|
121
|
+
def ==(other)
|
122
|
+
@file == other.file
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def mtime
|
128
|
+
(@requires + [@file]).compact.map do |fname|
|
129
|
+
File.mtime(fname)
|
130
|
+
end.reject{|t| t > Time.now }.max
|
131
|
+
end
|
132
|
+
|
133
|
+
# Figures out the full path of a required file.
|
134
|
+
def full_path(req)
|
135
|
+
dir = File.expand_path($LOAD_PATH.detect { |l| File.exists?(File.join(l, req)) })
|
136
|
+
File.join(dir, req)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Creates the reloader, assigns a +script+ to it and initially loads the
|
141
|
+
# application. Pass in the full path to the script, otherwise the script
|
142
|
+
# will be loaded relative to the current working directory.
|
143
|
+
def initialize(*scripts)
|
144
|
+
@scripts = []
|
145
|
+
update(*scripts)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Updates the reloader to only use the scripts provided:
|
149
|
+
#
|
150
|
+
# reloader.update("examples/blog.rb", "examples/wiki.rb")
|
151
|
+
def update(*scripts)
|
152
|
+
old = @scripts.dup
|
153
|
+
clear
|
154
|
+
@scripts = scripts.map do |script|
|
155
|
+
s = Script.new(script)
|
156
|
+
if pos = old.index(s)
|
157
|
+
# We already got a script, so we use the old (which might got a mtime)
|
158
|
+
old[pos]
|
159
|
+
else
|
160
|
+
s.load_apps
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Removes all the scripts from the reloader.
|
166
|
+
def clear
|
167
|
+
@scrips = []
|
168
|
+
end
|
169
|
+
|
170
|
+
# Simply calls reload! on all the Script objects.
|
171
|
+
def reload!
|
172
|
+
@scripts.each { |script| script.reload! }
|
173
|
+
end
|
174
|
+
|
175
|
+
# Returns a Hash of all the apps available in the scripts, where the key
|
176
|
+
# would be the name of the app (the one you gave to Camping.goes) and the
|
177
|
+
# value would be the app (wrapped inside App).
|
178
|
+
def apps
|
179
|
+
@scripts.inject({}) do |hash, script|
|
180
|
+
hash.merge(script.apps)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|