lapillus 0.0.2
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/examples/guest_book.rb +50 -0
- data/examples/hello_world.rb +12 -0
- data/examples/persons.rb +30 -0
- data/examples/tc_guest_book.rb +34 -0
- data/examples/tc_hello_world.rb +24 -0
- data/examples/tc_persons.rb +13 -0
- data/html/GuestBook.html +24 -0
- data/html/HelloWorld.html +5 -0
- data/html/LapillusTesterTestPage.html +12 -0
- data/html/Persons.html +7 -0
- data/html/TestPage.html +8 -0
- data/html/TestRemotePanel.html +8 -0
- data/lib/changelog.txt +6 -0
- data/lib/lapillus/base.rb +276 -0
- data/lib/lapillus/behaviours.rb +208 -0
- data/lib/lapillus/components.rb +116 -0
- data/lib/lapillus/containers.rb +234 -0
- data/lib/lapillus/dispatcher.rb +216 -0
- data/lib/lapillus/form_components.rb +88 -0
- data/lib/lapillus/lapillus_server.rb +21 -0
- data/lib/lapillus/lapillus_testers.rb +167 -0
- data/lib/lapillus/mongrel_server.rb +69 -0
- data/lib/lapillus/multiview.rb +45 -0
- data/lib/lapillus/pager.rb +104 -0
- data/lib/lapillus/process_upload.rb +11 -0
- data/lib/lapillus/web_application.rb +12 -0
- data/lib/lapillus/webrick_server.rb +90 -0
- data/lib/lapillus.rb +33 -0
- data/lib/license.txt +24 -0
- data/test/tc_base.rb +63 -0
- data/test/tc_behaviours.rb +106 -0
- data/test/tc_bookmarkablepagelink.rb +58 -0
- data/test/tc_components.rb +285 -0
- data/test/tc_containers.rb +173 -0
- data/test/tc_dispatcher.rb +106 -0
- data/test/tc_examples.rb +193 -0
- data/test/tc_form.rb +231 -0
- data/test/tc_fragments.rb +114 -0
- data/test/tc_hierarchy.rb +34 -0
- data/test/tc_lapillus.rb +30 -0
- data/test/tc_lapillus_testers.rb +108 -0
- data/test/tc_multiview.rb +91 -0
- data/test/tc_pager.rb +94 -0
- data/test/tc_rendering.rb +74 -0
- data/test/ts_all_test.rb +20 -0
- metadata +94 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
require 'cgi'
|
|
2
|
+
require 'cgi/session'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'thread'
|
|
5
|
+
|
|
6
|
+
class Dispatcher
|
|
7
|
+
# @@guard = Mutex.new
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
HTTPHEADER = { 'status'=> 'OK', 'Content-Type' => 'text/html; charset=utf-8'}
|
|
11
|
+
def Dispatcher.dispatch(cgi, application)
|
|
12
|
+
# @@guard.synchronize do
|
|
13
|
+
request_method = cgi.env_table['REQUEST_METHOD']
|
|
14
|
+
case request_method
|
|
15
|
+
when "GET"
|
|
16
|
+
Dispatcher.get(cgi, application)
|
|
17
|
+
when "POST"
|
|
18
|
+
Dispatcher.post(cgi, application)
|
|
19
|
+
else
|
|
20
|
+
raise "unexpected request method: "+request_method
|
|
21
|
+
end
|
|
22
|
+
# end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
#TODO: remove duplication between get and post
|
|
26
|
+
#TODO: set request cycle to nil when done
|
|
27
|
+
#TODO: remove duplication between new page and respond to event
|
|
28
|
+
def Dispatcher.get(cgi, application)
|
|
29
|
+
# log = WEBrick::Log.new
|
|
30
|
+
# log.info("URI path= #{cgi.env_table['PATH_INFO']}")
|
|
31
|
+
|
|
32
|
+
# puts;puts cgi.env_table.each {|k,v| puts k.to_s+":"+v.to_s};puts
|
|
33
|
+
|
|
34
|
+
session = Dispatcher.get_session(cgi)
|
|
35
|
+
|
|
36
|
+
request_cycle = RequestCycle.new(session)
|
|
37
|
+
RequestCycle.set(request_cycle)
|
|
38
|
+
path_info = cgi.env_table['PATH_INFO']
|
|
39
|
+
|
|
40
|
+
if cgi.has_key?'listener'
|
|
41
|
+
puts "AJAXREQUEST! "+cgi['listener'].to_s
|
|
42
|
+
|
|
43
|
+
pathparts=cgi.env_table['REQUEST_URI'].split('/')
|
|
44
|
+
if pathparts.length==2
|
|
45
|
+
page_identifier = application.homepage.to_s
|
|
46
|
+
else
|
|
47
|
+
pagepath = pathparts[2]
|
|
48
|
+
page_identifier = application.bookmarkable_pages[pagepath].to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
page = restore_page_from_session(page_identifier, path_info, session)
|
|
52
|
+
path_to_listener = cgi['listener']
|
|
53
|
+
listener = page[path_to_listener]
|
|
54
|
+
#TODO: this should be moved to the listener
|
|
55
|
+
#TODO: we should always store the session whether we
|
|
56
|
+
#rerender something or not
|
|
57
|
+
puts "!!"+listener.class.to_s
|
|
58
|
+
path_to_component = cgi['component']
|
|
59
|
+
#NOTICE: non existing cgi variables are return as empty string
|
|
60
|
+
if cgi.has_key?'menuitem'
|
|
61
|
+
listener.on_menu(cgi['menuitem'])
|
|
62
|
+
store_page_in_session(page,path_info,session)
|
|
63
|
+
path_to_redirect_to = cgi.env_table['REQUEST_URI']
|
|
64
|
+
path_to_redirect_to = path_to_redirect_to.split("?")[0]
|
|
65
|
+
puts "!!"+path_to_redirect_to
|
|
66
|
+
cgi.out({"status" => "302", "Location" => path_to_redirect_to}) do
|
|
67
|
+
""
|
|
68
|
+
end
|
|
69
|
+
# cgi.header("Location" => path_to_redirect_to)
|
|
70
|
+
# cgi.out(HTTPHEADER) { page.render }
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
else
|
|
74
|
+
if path_to_component!=""
|
|
75
|
+
component = page[path_to_component]
|
|
76
|
+
listener.on_drop(component)
|
|
77
|
+
else
|
|
78
|
+
listener.on_click
|
|
79
|
+
end
|
|
80
|
+
store_page_in_session(page,path_info,session)
|
|
81
|
+
|
|
82
|
+
cgi.out(HTTPHEADER) { listener.render_component }
|
|
83
|
+
return
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
pathparts=path_info.split('/')
|
|
88
|
+
if pathparts.length<=2
|
|
89
|
+
page_identifier = application.homepage.to_s
|
|
90
|
+
else
|
|
91
|
+
pagepath = pathparts[2]
|
|
92
|
+
page_identifier = application.bookmarkable_pages[pagepath].to_s
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# attribute = "pagemap_#{page_identifier}"
|
|
96
|
+
# page = session[attribute][1] if !session[attribute].nil?
|
|
97
|
+
page = session[path_info]
|
|
98
|
+
if !page.nil? #&& path_info == session[attribute][0]
|
|
99
|
+
render_page_and_store_in_session(request_cycle, page, session, cgi, path_info)
|
|
100
|
+
return
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
pathparts=path_info.split('/')
|
|
104
|
+
# puts "pathparts.length #{pathparts.length}"
|
|
105
|
+
# puts pathparts.join("::")
|
|
106
|
+
if (pathparts.length<2)
|
|
107
|
+
page = application.homepage.new
|
|
108
|
+
else
|
|
109
|
+
page_params = Hash.new
|
|
110
|
+
pagepath = pathparts[1]
|
|
111
|
+
(2..pathparts.length-1).step(2){|i| page_params[pathparts[i]]=pathparts[i+1]}
|
|
112
|
+
# puts pagepath
|
|
113
|
+
page = application.bookmarkable_pages[pagepath].new(page_params)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
render_page_and_store_in_session(request_cycle, page, session, cgi, path_info)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def Dispatcher.post(cgi, application)
|
|
120
|
+
session = Dispatcher.get_session(cgi)
|
|
121
|
+
|
|
122
|
+
request_cycle = RequestCycle.new(session)
|
|
123
|
+
RequestCycle.set=request_cycle
|
|
124
|
+
# puts "cgi_page=#{cgi['page'].string}"
|
|
125
|
+
page = restore_page_from_session(cgi['page'].string, cgi.env_table['PATH_INFO'], session)
|
|
126
|
+
|
|
127
|
+
values = Hash.new()
|
|
128
|
+
cgi.keys.each { |key| values[key] = cgi[key]}
|
|
129
|
+
page.post(values)
|
|
130
|
+
path_info = cgi.env_table['PATH_INFO']
|
|
131
|
+
render_page_and_store_in_session(request_cycle, page, session, cgi, path_info)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
def Dispatcher.get_session(cgi)
|
|
136
|
+
session = CGI::Session.new(cgi, 'database_manager' => CGI::Session::MemoryStore)
|
|
137
|
+
# puts "session id= #{session.session_id}"
|
|
138
|
+
return session
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def Dispatcher.render_page_and_store_in_session(request_cycle, page, session, cgi, path_info)
|
|
142
|
+
# puts "Dispatcher.render_page_and_store_in_session(#{request_cycle}, #{page}, #{session}, #{cgi}, #{path_info})"
|
|
143
|
+
if (request_cycle.response_page_set?)
|
|
144
|
+
page = request_cycle.response_page
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
store_page_in_session(page, path_info, session)
|
|
148
|
+
|
|
149
|
+
cgi.out(HTTPHEADER) { page.render }
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
#TODO: restrict the number of pages remembered
|
|
153
|
+
def Dispatcher.store_page_in_session(page, path_info, session)
|
|
154
|
+
# pagename = page.class.to_s #key
|
|
155
|
+
# page_info = [path_info, page] #value
|
|
156
|
+
# puts "Storing: "+pagename
|
|
157
|
+
# puts "STORING path_info: "+path_info
|
|
158
|
+
session[path_info] = page
|
|
159
|
+
# NOTE: We need to close the session before we can start streaming the output
|
|
160
|
+
session.close
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
#TODO: restrict the number of pages remembered
|
|
164
|
+
def Dispatcher.restore_page_from_session(page_identifier, path_info, session)
|
|
165
|
+
#puts "Restoring: "+page_identifier
|
|
166
|
+
# attribute = "pagemap_#{page_identifier}"
|
|
167
|
+
# puts "RESTORING path_info: "+path_info
|
|
168
|
+
# page_info = session[attribute]
|
|
169
|
+
# page = page_info[1]
|
|
170
|
+
# puts "session=#{session.to_yaml}"
|
|
171
|
+
# puts "path_info=[#{path_info}]"
|
|
172
|
+
page = session[path_info]
|
|
173
|
+
raise "page expired!" if (page==nil)
|
|
174
|
+
return page #if path_info == page_info[0]
|
|
175
|
+
# return nil
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class RequestCycle
|
|
181
|
+
@response_page
|
|
182
|
+
@session
|
|
183
|
+
|
|
184
|
+
def initialize(session)
|
|
185
|
+
super()
|
|
186
|
+
@session = session
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
#TODO: attr_accessor
|
|
190
|
+
def response_page= page
|
|
191
|
+
@response_page = page
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
#TODO: attr_accessor
|
|
195
|
+
def response_page
|
|
196
|
+
@response_page
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def response_page_set?
|
|
200
|
+
return response_page!=nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
#TODO: attr_reader
|
|
204
|
+
def session
|
|
205
|
+
@session
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def RequestCycle.get
|
|
209
|
+
current_request_cycle = Thread.current["current_request_cycle"]
|
|
210
|
+
return current_request_cycle
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def RequestCycle.set(request_cycle)
|
|
214
|
+
Thread.current["current_request_cycle"]= request_cycle
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
require 'lapillus/base'
|
|
2
|
+
|
|
3
|
+
module Lapillus
|
|
4
|
+
class FormComponent < Component
|
|
5
|
+
def post(values)
|
|
6
|
+
self.value=values[path] if values.has_key?(path)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render_to_element(element)
|
|
10
|
+
element.attributes['name'] = path
|
|
11
|
+
end
|
|
12
|
+
def value
|
|
13
|
+
return super if has_model?
|
|
14
|
+
return parent.model.send(identifier) if parent.has_model?
|
|
15
|
+
raise "model not set!"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def value=new_value
|
|
19
|
+
if property.nil?
|
|
20
|
+
@model=new_value
|
|
21
|
+
else
|
|
22
|
+
model.send(property.to_s+"=", new_value)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class FormTextComponent < FormComponent
|
|
29
|
+
def render_to_element(element)
|
|
30
|
+
super
|
|
31
|
+
text = value
|
|
32
|
+
text = '' if text.nil? # TODO: should this be done here, or is this the responsibility of the model ?
|
|
33
|
+
render_text(element, text)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
#TODO: current form test are not real life enough
|
|
37
|
+
#TODO: use stringio in test instead of string
|
|
38
|
+
def value=text
|
|
39
|
+
text = text.string if (text.kind_of?(StringIO))
|
|
40
|
+
super(text)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class TextField < FormTextComponent
|
|
45
|
+
def render_text(element, text)
|
|
46
|
+
element.add_attribute('value', text)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def Container.textfield(id, options={})
|
|
51
|
+
options.keys.each {|key|
|
|
52
|
+
raise "Unknown key: #{key}" if key!=:model and key!=:property
|
|
53
|
+
}
|
|
54
|
+
internal_add_component(id) { TextField.new(id, options[:model]||"", options[:property]) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
class TextArea < FormTextComponent
|
|
58
|
+
def render_text(element, text)
|
|
59
|
+
element.text = text
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def Container.textarea(id, options={})
|
|
64
|
+
options.keys.each {|key|
|
|
65
|
+
raise "Unknown key: #{key}" if key!=:model and key!=:property
|
|
66
|
+
}
|
|
67
|
+
internal_add_component(id) { TextArea.new(id, options[:model]||"", options[:property]) }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class FileUploadField < FormComponent
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def Container.fileuploadfield(id, options={})
|
|
74
|
+
options.keys.each {|key|
|
|
75
|
+
raise "Unknown key: #{key}" if key!=:model and key!=:property
|
|
76
|
+
}
|
|
77
|
+
internal_add_component(id) { FileUploadField.new(id, options[:model]||"", options[:property]) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class PasswordTextField < TextField
|
|
81
|
+
def reset_password(bool)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def Container.password_textfield(id, model="", options={})
|
|
86
|
+
internal_add_component(id) { PasswordTextField.new(id, model, options[:property]) }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'lapillus'
|
|
2
|
+
require 'lapillus/webrick_server'
|
|
3
|
+
|
|
4
|
+
class LapillusServer
|
|
5
|
+
attr_reader :server
|
|
6
|
+
def initialize(params={})
|
|
7
|
+
root_url = params[:root_url]
|
|
8
|
+
homepage = params[:homepage]
|
|
9
|
+
port = params[:port]
|
|
10
|
+
raise "root_url not set!" if root_url.nil?
|
|
11
|
+
raise "homepage not set!" if homepage.nil?
|
|
12
|
+
raise "port not set!" if port.nil?
|
|
13
|
+
@server = WebrickServer.new(port)
|
|
14
|
+
web_application = WebApplication.new
|
|
15
|
+
web_application.homepage = homepage
|
|
16
|
+
server.mount(root_url, web_application)
|
|
17
|
+
end
|
|
18
|
+
def start
|
|
19
|
+
server.start
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
require 'test/unit'
|
|
2
|
+
require 'lapillus'
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
class LapillusTester
|
|
6
|
+
include Test::Unit::Assertions
|
|
7
|
+
attr_reader :html
|
|
8
|
+
|
|
9
|
+
@page_class
|
|
10
|
+
@page_instance
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
super()
|
|
14
|
+
RequestCycle.set(RequestCycle.new(Hash.new))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def start_page(page)
|
|
18
|
+
@page_class = page
|
|
19
|
+
@page_instance = @page_class.new()
|
|
20
|
+
render
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def start_page_instance(page_instance)
|
|
24
|
+
@page_class = page_instance.class
|
|
25
|
+
@page_instance = page_instance
|
|
26
|
+
render
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#TODO: should this method be public?
|
|
30
|
+
def render
|
|
31
|
+
@html = @page_instance.render
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def assert_component(path, expected_component_class)
|
|
35
|
+
component = get_component_from_last_rendered_page(path)
|
|
36
|
+
assert_kind_of(expected_component_class, component,"Component #{component} on path #{path} is not a(n) #{expected_component_class}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def assert_contains(expected_content, actual_content=@html)
|
|
40
|
+
full_message = build_message(nil, "content ? not found, but was ? ", expected_content, actual_content)
|
|
41
|
+
assert_block full_message do
|
|
42
|
+
!@html[expected_content].nil?
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def assert_error_messages(expected_error_messages)
|
|
47
|
+
# TODO: implement assert_error_messages(expected_error_messages)
|
|
48
|
+
flunk 'You need to implement assert_error_messages(expected_error_messages) first!'
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def assert_expire_previous_page
|
|
52
|
+
# TODO: implement assert_expire_previous_page
|
|
53
|
+
flunk 'You need to implement assert_expire_previous_page first!'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def assert_info_messages(expected_info_messages)
|
|
57
|
+
# TODO: implement assert_info_messages(expected_info_messages)
|
|
58
|
+
flunk 'You need to implement assert_info_messages(expected_info_messages) first!'
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def assert_invisible(path)
|
|
62
|
+
# TODO: implement assert_invisible(path)
|
|
63
|
+
flunk 'You need to implement assert_invisible(path) first!'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def assert_label(path, expected_label_text)
|
|
67
|
+
assert_component(path, Lapillus::Label)
|
|
68
|
+
assert_equal(expected_label_text, get_component_from_last_rendered_page(path).value)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def assert_list_view(path, expected_list)
|
|
72
|
+
# TODO: implement assert_list_view(path, expected_list)
|
|
73
|
+
flunk 'You need to implement assert_list_view(path, expected_list) first!'
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def assert_no_error_message
|
|
77
|
+
# TODO: implement assert_no_error_message
|
|
78
|
+
flunk 'You need to implement assert_no_error_message first!'
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def assert_no_info_message
|
|
82
|
+
# TODO: implement assert_no_info_message
|
|
83
|
+
flunk 'You need to implement assert_no_info_message first!'
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def assert_page_link(path, expected_page_class)
|
|
87
|
+
click_link(path)
|
|
88
|
+
assert_rendered_page(expected_page_class)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def assert_rendered_page(expected_rendered_page_class)
|
|
92
|
+
assert_kind_of(expected_rendered_page_class, @page_instance)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def assert_visible(path)
|
|
96
|
+
# TODO: implement assert_visible(path)
|
|
97
|
+
flunk 'You need to implement assert_visible(path) first!'
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def click_link(path)
|
|
101
|
+
link = get_component_from_last_rendered_page(path)
|
|
102
|
+
assert_not_nil(link,"Link <#{path}> not found!")
|
|
103
|
+
page_parameters = link.page_parameters
|
|
104
|
+
@page_instance = @page_class.new(page_parameters) # TODO: page can be found in the link!
|
|
105
|
+
@html = @page_instance.render
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def debug_component_trees
|
|
109
|
+
# TODO: implement debug_component_trees
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def dump_page
|
|
113
|
+
puts @html
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def get_component_from_last_rendered_page(path)
|
|
117
|
+
path_parts=path.split(":")
|
|
118
|
+
component=@page_instance[path_parts[0]]
|
|
119
|
+
assert_not_nil(component, "No component found on path '#{path_parts[0]}'")
|
|
120
|
+
(1..path_parts.size-1).each { |i|
|
|
121
|
+
component=component[path_parts[i]]
|
|
122
|
+
assert_not_nil(component, "No component found on path '#{path_parts.slice(0..i).join(':')}'")
|
|
123
|
+
}
|
|
124
|
+
component
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def get_messages(level)
|
|
128
|
+
# TODO: implement get_messages(level)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def new_form_tester(path)
|
|
132
|
+
form = get_component_from_last_rendered_page(path)
|
|
133
|
+
assert_not_nil(form, "Form <#{path}> not found!")
|
|
134
|
+
return FormTester.new(form, self)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
class FormTester
|
|
139
|
+
include Test::Unit::Assertions
|
|
140
|
+
|
|
141
|
+
@values
|
|
142
|
+
|
|
143
|
+
def initialize(form, tester)
|
|
144
|
+
@form = form
|
|
145
|
+
@values = Hash[]
|
|
146
|
+
@tester = tester
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def set_value(key, value)
|
|
150
|
+
field = @form[key]
|
|
151
|
+
assert_not_nil(field, "TextField <#{key}> in Form not found!")
|
|
152
|
+
@values["#{@form.identifier}.#{key}"] = value
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def submit(button=nil)
|
|
156
|
+
request_cycle = RequestCycle.new(Hash.new())
|
|
157
|
+
RequestCycle.set(request_cycle)
|
|
158
|
+
@values['submit']= StringIO.new(button) if !button.nil?
|
|
159
|
+
@form.post(@values)
|
|
160
|
+
@tester.render
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def model
|
|
164
|
+
@form.model
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'mongrel'
|
|
3
|
+
require 'cgi'
|
|
4
|
+
require 'active_record'
|
|
5
|
+
|
|
6
|
+
class MongrelServer
|
|
7
|
+
|
|
8
|
+
def initialize(port)
|
|
9
|
+
$stdout.sync=true
|
|
10
|
+
$stderr.sync=true
|
|
11
|
+
@port = port
|
|
12
|
+
@server = Mongrel::HttpServer.new("0.0.0.0", @port)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def start
|
|
16
|
+
trap("INT"){ @server.stop }
|
|
17
|
+
trap('TERM'){ @server.stop }
|
|
18
|
+
@server.run.join
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mount(name, application)
|
|
22
|
+
@server.register(name, MongrelCGIWrapper.new(application))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def mount_context(url, directory)
|
|
26
|
+
@server.register(url, Mongrel::DirHandler.new(directory))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def allow_termination_for_test
|
|
30
|
+
Mongrel::Terminate.set_server=@server
|
|
31
|
+
@server.register("/terminate", Mongrel::Terminate.new)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class MongrelCGIWrapper < Mongrel::HttpHandler
|
|
36
|
+
@application
|
|
37
|
+
|
|
38
|
+
def initialize(application)
|
|
39
|
+
super()
|
|
40
|
+
@application = application
|
|
41
|
+
ActiveRecord::Base.allow_concurrency=true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def process(request, response)
|
|
45
|
+
cgi = Mongrel::CGIWrapper.new(request, response)
|
|
46
|
+
begin
|
|
47
|
+
Dispatcher.dispatch(cgi,@application)
|
|
48
|
+
rescue => e
|
|
49
|
+
STDERR.puts e
|
|
50
|
+
STDERR.puts e.backtrace.join("\n")
|
|
51
|
+
ensure
|
|
52
|
+
ActiveRecord::Base.clear_active_connections!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
module Mongrel
|
|
58
|
+
class Terminate < Mongrel::HttpHandler
|
|
59
|
+
def Terminate.set_server=server
|
|
60
|
+
@@server = server
|
|
61
|
+
end
|
|
62
|
+
def process(request, response)
|
|
63
|
+
puts "trying to shutdown server"
|
|
64
|
+
@@server.stop
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require 'lapillus/containers'
|
|
2
|
+
|
|
3
|
+
class SingleView < Lapillus::Fragment
|
|
4
|
+
def initialize(id)
|
|
5
|
+
super(id, id)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class MultiView < Lapillus::Component
|
|
10
|
+
attr_accessor :mode
|
|
11
|
+
def initialize(id, views)
|
|
12
|
+
super(id)
|
|
13
|
+
@views = views
|
|
14
|
+
views.each {|view| view.parent = self } #TODO: add test!
|
|
15
|
+
@mode=views[0].identifier
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_view(id=mode)
|
|
19
|
+
@views.each do |v|
|
|
20
|
+
return v if v.identifier == id
|
|
21
|
+
end
|
|
22
|
+
raise "view with identifier #{id} not found!"
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render_container(html)
|
|
27
|
+
singleview = current_view
|
|
28
|
+
result = singleview.render_container(html)
|
|
29
|
+
render_behaviours(result)
|
|
30
|
+
return result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# TODO: add test
|
|
34
|
+
def post(values)
|
|
35
|
+
current_view.post(values)
|
|
36
|
+
end
|
|
37
|
+
# TODO: add test
|
|
38
|
+
def component(identifier)
|
|
39
|
+
return current_view.component(identifier)
|
|
40
|
+
end
|
|
41
|
+
# TODO: add test
|
|
42
|
+
def [](path)
|
|
43
|
+
current_view[path]
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
require 'lapillus'
|
|
2
|
+
|
|
3
|
+
module Lapillus
|
|
4
|
+
|
|
5
|
+
class Pager < Container
|
|
6
|
+
attr_reader :current, :page_container
|
|
7
|
+
attr_accessor :pages, :navigation, :first_button, :previous_button
|
|
8
|
+
attr_accessor :next_button, :last_button, :position
|
|
9
|
+
def initialize(id, pages, &block)
|
|
10
|
+
super(id)
|
|
11
|
+
raise "no block given!" if !block_given?
|
|
12
|
+
@pages = pages
|
|
13
|
+
@current = 1
|
|
14
|
+
@block = block
|
|
15
|
+
@navigation = Lapillus::Container.new("navigation")
|
|
16
|
+
@first_button = FirstButton.new("first", self)
|
|
17
|
+
@previous_button = PreviousButton.new("previous", self)
|
|
18
|
+
@next_button = NextButton.new("next", self)
|
|
19
|
+
@last_button = LastButton.new("last", self)
|
|
20
|
+
@position = Label.new("position")
|
|
21
|
+
navigation.add(first_button)
|
|
22
|
+
navigation.add(previous_button)
|
|
23
|
+
navigation.add(next_button)
|
|
24
|
+
navigation.add(last_button)
|
|
25
|
+
navigation.add(position)
|
|
26
|
+
build_hierarchy
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#TODO: this is not so handy
|
|
30
|
+
#components are recreated and stuff; split in constructor/on_render
|
|
31
|
+
#also parent and path do not work
|
|
32
|
+
def build_hierarchy
|
|
33
|
+
@page_container = ListItem.new("page")
|
|
34
|
+
@page_container.parent = self #NOTE: I could do this in the constructor!
|
|
35
|
+
if !@pages.empty?
|
|
36
|
+
@page_container.instance_exec(page_content, &@block)
|
|
37
|
+
# @components << @page_container ???
|
|
38
|
+
first_button.visible= previous_button.visible = current > 1
|
|
39
|
+
last_button.visible= next_button.visible = current < pages.size
|
|
40
|
+
position.model = "#{@current} van #{@pages.size}"
|
|
41
|
+
page_container.visible = true
|
|
42
|
+
navigation.visible = true
|
|
43
|
+
else
|
|
44
|
+
page_container.visible = false
|
|
45
|
+
navigation.visible = false
|
|
46
|
+
end
|
|
47
|
+
@components = []
|
|
48
|
+
add(page_container)
|
|
49
|
+
add(navigation)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
#TODO: rewrite using closures
|
|
53
|
+
class PagerButton < Lapillus::AjaxLink
|
|
54
|
+
def initialize(id, pager)
|
|
55
|
+
super(id, pager) #pager is the container to be refreshed!
|
|
56
|
+
@pager = pager
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class FirstButton < PagerButton
|
|
61
|
+
def on_click
|
|
62
|
+
@pager.current = 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
class PreviousButton < PagerButton
|
|
67
|
+
def on_click
|
|
68
|
+
@pager.current -= 1
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
class NextButton < PagerButton
|
|
73
|
+
def on_click
|
|
74
|
+
@pager.current += 1
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class LastButton < PagerButton
|
|
79
|
+
def on_click
|
|
80
|
+
@pager.current = @pager.pages.size
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def page_content
|
|
85
|
+
@pages[@current-1]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def current=(current)
|
|
89
|
+
@current = current
|
|
90
|
+
build_hierarchy
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# #NOTE: only for test.. see NOTE build.hierarchy
|
|
94
|
+
# def get_component(path)
|
|
95
|
+
#
|
|
96
|
+
# super
|
|
97
|
+
# end
|
|
98
|
+
|
|
99
|
+
# #TODO: only tested indirectly
|
|
100
|
+
# def render_container(element)
|
|
101
|
+
# super
|
|
102
|
+
# end
|
|
103
|
+
end
|
|
104
|
+
end
|