ruote-kit 2.2.0.3 → 2.3.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.
Files changed (60) hide show
  1. data/CHANGELOG.txt +28 -1
  2. data/CREDITS.txt +8 -2
  3. data/LICENSE.txt +1 -1
  4. data/Rakefile +21 -9
  5. data/lib/ruote-kit.rb +13 -4
  6. data/lib/ruote-kit/core_ext.rb +15 -0
  7. data/lib/ruote-kit/helpers/json_helpers.rb +22 -7
  8. data/lib/ruote-kit/helpers/link_helpers.rb +9 -7
  9. data/lib/ruote-kit/helpers/pagination_helpers.rb +1 -2
  10. data/lib/ruote-kit/helpers/render_helpers.rb +1 -1
  11. data/lib/ruote-kit/public/_ruote/images/favicon.png +0 -0
  12. data/lib/ruote-kit/public/_ruote/images/{ruote_buttons.png → ruote-buttons.png} +0 -0
  13. data/lib/ruote-kit/public/_ruote/images/ruote.png +0 -0
  14. data/lib/ruote-kit/public/_ruote/javascripts/foolbox-all.min.js +3 -0
  15. data/lib/ruote-kit/public/_ruote/javascripts/jquery-1.9.1.min.js +5 -0
  16. data/lib/ruote-kit/public/_ruote/javascripts/rk.js +68 -17
  17. data/lib/ruote-kit/public/_ruote/javascripts/ruote-fluo-all.min.js +3 -0
  18. data/lib/ruote-kit/public/_ruote/stylesheets/rk.css +82 -12
  19. data/lib/ruote-kit/public/_ruote/stylesheets/ruote-buttons.png +0 -0
  20. data/lib/ruote-kit/public/_ruote/stylesheets/ruote-fluo-editor.css +9 -6
  21. data/lib/ruote-kit/public/_ruote/stylesheets/ruote-fluo.css +62 -0
  22. data/lib/ruote-kit/resources/errors.rb +8 -3
  23. data/lib/ruote-kit/resources/expressions.rb +38 -35
  24. data/lib/ruote-kit/resources/participants.rb +1 -1
  25. data/lib/ruote-kit/resources/processes.rb +61 -4
  26. data/lib/ruote-kit/resources/workitems.rb +4 -4
  27. data/lib/ruote-kit/version.rb +1 -1
  28. data/lib/ruote-kit/views/_pagination.html.haml +2 -2
  29. data/lib/ruote-kit/views/_tree_editor.html.haml +63 -30
  30. data/lib/ruote-kit/views/error.html.haml +50 -9
  31. data/lib/ruote-kit/views/errors.html.haml +1 -1
  32. data/lib/ruote-kit/views/expression.html.haml +83 -33
  33. data/lib/ruote-kit/views/expressions.html.haml +23 -8
  34. data/lib/ruote-kit/views/http_error.html.haml +9 -2
  35. data/lib/ruote-kit/views/layout.html.haml +14 -13
  36. data/lib/ruote-kit/views/process.html.haml +75 -20
  37. data/lib/ruote-kit/views/processes.html.haml +9 -3
  38. data/lib/ruote-kit/views/processes_new.html.haml +19 -2
  39. data/lib/ruote-kit/views/schedules.html.haml +2 -2
  40. data/lib/ruote-kit/views/workitem.html.haml +17 -6
  41. data/ruote-kit.gemspec +9 -7
  42. data/spec/cases/orphan_workitem_spec.rb +141 -0
  43. data/spec/core_ext_spec.rb +19 -0
  44. data/spec/resources/errors_spec.rb +182 -156
  45. data/spec/resources/expressions_spec.rb +497 -379
  46. data/spec/resources/index_spec.rb +5 -5
  47. data/spec/resources/participants_spec.rb +64 -75
  48. data/spec/resources/processes_spec.rb +396 -277
  49. data/spec/resources/schedules_spec.rb +92 -76
  50. data/spec/resources/workitems_spec.rb +352 -343
  51. data/spec/ruote-kit_configure_spec.rb +41 -13
  52. data/spec/spec_helper.rb +6 -8
  53. data/spec/support/link_helper.rb +11 -0
  54. data/spec/webapp_helpers_spec.rb +29 -23
  55. metadata +161 -180
  56. data/README.rdoc +0 -313
  57. data/lib/ruote-kit/public/_ruote/images/favicon.ico +0 -0
  58. data/lib/ruote-kit/public/_ruote/javascripts/jquery-1.4.2.min.js +0 -154
  59. data/lib/ruote-kit/public/_ruote/javascripts/ruote-fluo-editor.js +0 -548
  60. data/lib/ruote-kit/public/_ruote/javascripts/ruote-fluo.js +0 -1118
@@ -5,31 +5,46 @@
5
5
 
6
6
  %h1 expressions for #{@process.wfid}
7
7
 
8
- %canvas#fluo
8
+ #fluo
9
+
10
+ %table.list.expressions
9
11
 
10
- %table.list{ :style => 'width: auto;', :onmouseout => "Fluo.highlight('fluo')" }
11
12
  %thead
12
13
  %td fei
13
14
  %td expid
14
15
  %td exp
15
16
  %td applied
17
+ %td state
16
18
 
17
19
 
18
20
  - @process.expressions.each do |expression|
19
- %tr{ :onmouseover => "Fluo.highlight('fluo', '#{expression.fei.expid}')" }
21
+ %tr{ 'data-rk-expid' => expression.fei.expid }
20
22
  %td
21
- = alink(:expressions, expression.fei.sid)
23
+ = alink(:expressions, expression.fei.sid, :text => [ expression.fei.wfid, expression.fei.expid ].join(' '))
22
24
  %td
23
25
  &= expression.fei.expid
24
26
  %td
25
27
  &= expression.tree.first
26
28
  %td
27
29
  = Time.parse(expression.h.put_at).localtime.strftime('%Y/%m/%d %H:%M:%S')
30
+ %td
31
+ = expression.state
28
32
 
29
33
  :javascript
30
34
 
31
- var proc_rep = #{process_tree(@process)};
32
- Fluo.renderFlow('fluo', proc_rep);
33
- Fluo.crop('fluo');
34
- Fluo.resizeForMaxWidth('fluo', $('body').width() / 2.1);
35
+ $(document).ready(function() {
36
+
37
+ Rk.fitFluo();
38
+
39
+ var proc_rep = #{process_tree(@process)};
40
+ RuoteFluo.render('fluo', proc_rep, { fit: 'down' });
41
+
42
+ $('table.list').on('mouseout', function(ev) {
43
+ RuoteFluo.highlight('#fluo');
44
+ });
45
+ $('tr').on('mouseover', function(ev) {
46
+ $tr = $(ev.target).closest('tr');
47
+ RuoteFluo.highlight('#fluo', $tr.attr('data-rk-expid'));
48
+ });
49
+ });
35
50
 
@@ -4,8 +4,15 @@
4
4
  %h1 #{@code} #{@message}
5
5
 
6
6
  - if @trace
7
- %pre.trace{ :onclick => 'Rk.toggleNext(this);' } #{h(@trace.first.split("\n").first + '...')}
8
- %pre.trace{ :onclick => 'Rk.toggleNext(this);', :style => 'display: none;' } #{h(@trace.join("\n"))}
7
+ %pre.trace #{h(@trace.first.split("\n").first + '...')}
8
+ %pre.trace{ :style => 'display: none;' } #{h(@trace.join("\n"))}
9
9
 
10
10
  %a{ :href => "javascript:history.go(-1)" } back
11
11
 
12
+ :javascript
13
+
14
+ $(document).ready(function() {
15
+
16
+ $('pre.trace').on('click', Rk.toggleNext);
17
+ });
18
+
@@ -7,26 +7,27 @@
7
7
 
8
8
  %title ruote-kit
9
9
 
10
- %link{ :type => 'image/vnd.microsoft.icon', :href => '/_ruote/images/favicon.ico', :rel => 'icon' }
10
+ %link{ :type => 'image/png', :href => url('/_ruote/images/favicon.png'), :rel => 'icon' }
11
11
 
12
- %link{ :type => 'text/css', :href => '/_ruote/stylesheets/reset.css', :rel => 'stylesheet' }
13
- %link{ :type => 'text/css', :href => '/_ruote/stylesheets/rk.css', :rel => 'stylesheet' }
12
+ %link{ :type => 'text/css', :href => url('/_ruote/stylesheets/reset.css'), :rel => 'stylesheet' }
13
+ %link{ :type => 'text/css', :href => url('/_ruote/stylesheets/rk.css'), :rel => 'stylesheet' }
14
14
 
15
- %script{ :type => 'text/javascript', :src => '/_ruote/javascripts/jquery-1.4.2.min.js' }
16
- %script{ :type => 'text/javascript', :src => '/_ruote/javascripts/rk.js' }
15
+ %script{ :type => 'text/javascript', :src => url('/_ruote/javascripts/jquery-1.9.1.min.js') }
16
+ %script{ :type => 'text/javascript', :src => url('/_ruote/javascripts/rk.js') }
17
17
 
18
18
  - if @_uses_fluo
19
- %script{ :type => 'text/javascript', :src => '/_ruote/javascripts/ruote-fluo.js' }
20
- %script{ :type => 'text/javascript', :src => '/_ruote/javascripts/ruote-fluo-editor.js' }
21
- %link{ :type => 'text/css', :href => '/_ruote/stylesheets/ruote-fluo-editor.css', :rel => 'stylesheet' }
22
- - #%script{ :type => "text/javascript", :src => "/_ruote/javascripts/fluo-dial.js" }
19
+ %script{ :type => 'text/javascript', :src => url('/_ruote/javascripts/foolbox-all.min.js') }
20
+ %script{ :type => 'text/javascript', :src => url('/_ruote/javascripts/ruote-fluo-all.min.js') }
21
+ %link{ :type => 'text/css', :href => url('/_ruote/stylesheets/ruote-fluo.css'), :rel => 'stylesheet' }
22
+ %link{ :type => 'text/css', :href => url('/_ruote/stylesheets/ruote-fluo-editor.css'), :rel => 'stylesheet' }
23
23
 
24
24
  %body
25
25
  #container
26
26
 
27
27
  #header
28
28
  #hlogo
29
- %img{ :src => '/_ruote/images/ruote.png' }
29
+ %a{ :href => url('/_ruote') }
30
+ %img{ :src => url('/_ruote/images/ruote.png') }
30
31
  #htext
31
32
  #ruote_kit
32
33
  ruote-kit
@@ -41,7 +42,7 @@
41
42
  |
42
43
  = alink(:participants, :text => 'participants')
43
44
  ||
44
- %a{ :href => '/_ruote/processes/new'} launch process
45
+ %a{ :href => url('/_ruote/processes/new') } launch process
45
46
  #hclear
46
47
 
47
48
  #main
@@ -54,11 +55,11 @@
54
55
 
55
56
  #footer
56
57
  %div
57
- © 2009-2011 Kenneth Kalmer
58
+ © 2009-2013 Kenneth Kalmer
58
59
  |
59
60
  %a{ :href => 'http://www.opensourcery.co.za' } blog
60
61
  |
61
- %a{ :href => 'http://github.com/tosch/ruote-kit' } source
62
+ %a{ :href => 'http://github.com/kennethkalmer/ruote-kit' } source
62
63
  |
63
64
  %a{ :href => 'http://ruote.rubyforge.org' } powered by ruote
64
65
 
@@ -7,7 +7,7 @@
7
7
  process
8
8
  = @process.wfid
9
9
 
10
- %canvas#fluo
10
+ #fluo
11
11
 
12
12
  %table.details
13
13
  %tr
@@ -23,7 +23,7 @@
23
23
  - @process.position.each do |pos|
24
24
  - text = "#{pos[1]} #{pos[2]['task']}"
25
25
  - expid = pos.first.split('!').first
26
- %span{ :onmouseover => "Fluo.highlight('fluo', '#{expid}');", :onmouseout => "Fluo.highlight('fluo');" }
26
+ %span.position{ 'data-rk-expid' => expid }
27
27
  - if @process.stored_workitems.find { |wi| wi.fei.sid == pos.first }
28
28
  = alink(:workitems, pos.first, :text => text)
29
29
  - else
@@ -55,39 +55,64 @@
55
55
  %td
56
56
  launch time
57
57
  %td
58
- = Time.parse(@process.launched_time).localtime.strftime('%Y/%m/%d %H:%M:%S')
58
+ - if @process.launched_time
59
+ = Time.parse(@process.launched_time).localtime.strftime('%Y/%m/%d %H:%M:%S')
60
+ - else
61
+ ?
59
62
  %tr
60
63
  %td
61
64
  last active
62
65
  %td
63
- = Time.parse(@process.last_active).localtime.strftime('%Y/%m/%d %H:%M:%S')
66
+ - if @process.last_active
67
+ = Time.parse(@process.last_active).localtime.strftime('%Y/%m/%d %H:%M:%S')
68
+ - else
69
+ ?
70
+ %tr
71
+ %td
72
+ root exp state
73
+ %td
74
+ = @process.root_expression_state
64
75
  %tr
65
76
  %td
66
77
  tags
67
78
  %td
68
- &= @process.tags.collect { |ta| ta.first }.join(', ')
69
-
79
+ &= (@process.tags || []).collect { |ta| ta.first }.join(', ')
80
+
70
81
  %tr
71
82
  %td.no_border{ :colspan => 2 }
72
83
   
73
-
74
84
  %tr
75
- %td
76
- cancel expression
77
85
  %td
78
86
  %form{ :method => 'post' }
79
- %input{ :type => 'hidden', :name => '_method', :value => 'delete' }
80
- %input{ :type => 'submit', :value => 'cancel', :onclick => 'return confirm("really ?");' }
81
- %span.description DELETE /_ruote/processes/#{@process.wfid}
87
+ %input{ :type => 'hidden', :name => '_method', :value => 'PUT' }
88
+ - if @process.root_expression_state == 'paused'
89
+ %input{ :type => 'hidden', :name => 'state', :value => 'resuming' }
90
+ %input{ :type => 'submit', :value => 'resume' }
91
+ - else
92
+ %input{ :type => 'hidden', :name => 'state', :value => 'paused' }
93
+ %input{ :type => 'submit', :value => 'pause' }
94
+ %td
95
+ %span.description
96
+ - if @process.root_expression_state == 'paused'
97
+ PUT /_ruote/processes/#{@process.wfid}?state=resuming
98
+ - else
99
+ PUT /_ruote/processes/#{@process.wfid}?state=paused
100
+
82
101
  %tr
83
102
  %td
84
- kill expression
103
+ %form{ :method => 'post' }
104
+ %input{ :type => 'hidden', :name => '_method', :value => 'DELETE' }
105
+ %input{ :type => 'submit', :value => 'cancel' }
106
+ %td
107
+ %span.description DELETE /_ruote/processes/#{@process.wfid}
108
+ %tr
85
109
  %td
86
110
  %form{ :method => 'post' }
87
- %input{ :type => 'hidden', :name => '_method', :value => 'delete' }
111
+ %input{ :type => 'hidden', :name => '_method', :value => 'DELETE' }
88
112
  %input{ :type => 'hidden', :name => '_kill', :value => '1' }
89
- %input{ :type => 'submit', :value => 'kill', :onclick => 'return confirm("really ?");' }
90
- %span.description DELETE /_ruote/processes/#{@process.wfid}?_kill=1
113
+ %input{ :type => 'submit', :value => 'kill' }
114
+ %td
115
+ %span.description DELETE /_ruote/processes/#{@process.wfid}?_kill=1
91
116
  %tr
92
117
  %td
93
118
  expressions
@@ -115,8 +140,38 @@
115
140
 
116
141
  :javascript
117
142
 
118
- var proc_rep = #{process_tree(@process)};
119
- Fluo.renderFlow('fluo', proc_rep);
120
- Fluo.crop('fluo');
121
- Fluo.resizeForMaxWidth('fluo', $('body').width() / 2.1);
143
+ $(document).ready(function() {
144
+
145
+ //
146
+ // confirmation dialogs
147
+
148
+ $('input[type="submit"]').on('click', function(ev) {
149
+ return confirm("really " + ev.target.value + " process?");
150
+ });
151
+
152
+ //
153
+ // fluo
154
+
155
+ Rk.fitFluo();
156
+
157
+ var proc_rep = #{process_tree(@process)};
158
+ var pins = #{Rufus::Json.encode(@pins)};
159
+
160
+ RuoteFluo.render('#fluo', proc_rep, { fit: 'down' });
161
+ Rk.onClickZoom('#fluo');
162
+
163
+ pins.forEach(function(pin) {
164
+ RuoteFluo.pin('#fluo', pin[0], pin[1], pin[2])
165
+ });
166
+
167
+ // onmouseover for "positions"
168
+
169
+ $('span.position').on('mouseover', function(ev) {
170
+ var $span = $(ev.target).closest('span');
171
+ Fluo.highlight('#fluo', $span.attr('data-rk-expid'));
172
+ });
173
+ $('span.position').on('mouseout', function(ev) {
174
+ Fluo.highlight('#fluo');
175
+ });
176
+ });
122
177
 
@@ -13,6 +13,8 @@
13
13
  name
14
14
  %td
15
15
  position
16
+ %td
17
+ state
16
18
  %td
17
19
  tags
18
20
  %td
@@ -42,11 +44,15 @@
42
44
  - else
43
45
  &= text
44
46
  %td
45
- &= process.tags.collect { |ta| ta.first }.join(', ')
47
+ = process.root_expression_state
48
+ %td
49
+ &= (process.tags || []).collect { |ta| ta.first }.join(', ')
46
50
  %td
47
- = Time.parse(process.launched_time).localtime.strftime('%Y/%m/%d %H:%M:%S')
51
+ - if process.launched_time
52
+ = Time.parse(process.launched_time).localtime.strftime('%Y/%m/%d %H:%M:%S')
48
53
  %td
49
- = Time.parse(process.last_active).localtime.strftime('%Y/%m/%d %H:%M:%S')
54
+ - if process.last_active
55
+ = Time.parse(process.last_active).localtime.strftime('%Y/%m/%d %H:%M:%S')
50
56
  %td.right{ :title => 'expressions' }
51
57
  = alink(:expressions, process.wfid, :text => process.expressions.size)
52
58
  %td.right{ :title => 'errors' }
@@ -13,7 +13,7 @@
13
13
  %div
14
14
  %label
15
15
  process definition
16
- %a.ruote_button.ruote_go_button{ :title => 'open process definition editor', :href => '', :onclick => 'showEditor(); return false;' }
16
+ %a.ruote_button.ruote_go_button{ :title => 'open process definition editor' }
17
17
 
18
18
  %textarea#tree{ :name => 'definition', :cols => 80, :rows => 10 } #{sample_process}
19
19
 
@@ -23,7 +23,7 @@
23
23
  %textarea{ :name => 'fields', :cols => 80, :rows => 3 }
24
24
 
25
25
  %div
26
- %label.togglable{ :onclick => 'Rk.toggle(this, $("textarea.variables")[0], "(process variables)", "process variables");' }
26
+ %label.togglable
27
27
  (process variables)
28
28
  %textarea.variables{ :name => 'variables', :cols => 80, :rows => 3 }
29
29
 
@@ -32,5 +32,22 @@
32
32
  or
33
33
  %a{ :href => '/_ruote/processes' } cancel
34
34
 
35
+ :javascript
36
+
37
+ $('label.togglable').on('click', function(ev) {
38
+
39
+ Rk.toggle(
40
+ ev.target,
41
+ $("textarea.variables")[0],
42
+ "(process variables)",
43
+ "process variables");
44
+ });
45
+
46
+ $('a.ruote_go_button').on('click', function(ev) {
47
+
48
+ RkEditor.show();
49
+ return false;
50
+ });
51
+
35
52
  = haml :_tree_editor, :layout => false
36
53
 
@@ -6,7 +6,7 @@
6
6
  = haml :_pagination, :layout => false
7
7
 
8
8
  %table.list
9
-
9
+
10
10
  %thead
11
11
  %td
12
12
  owner / [ target ]
@@ -26,7 +26,7 @@
26
26
  %td
27
27
  %div
28
28
  = alink(:expressions, schedule['owner'].sid)
29
- - if schedule['owner'] != schedule['target']
29
+ - if schedule['target'] && schedule['target'] != schedule['owner']
30
30
  %div
31
31
  = alink(:expressions, schedule['target'].sid)
32
32
  %td
@@ -7,11 +7,11 @@
7
7
  workitem
8
8
  = @workitem.fei.sid
9
9
 
10
- %canvas#fluo
10
+ #fluo
11
11
 
12
12
  %form{ :method => 'POST', :enctype => 'multipart/form-data', :accept_charset => 'UTF-8' }
13
13
 
14
- %input{ :type => 'hidden', :name => '_method', :value => 'put' }
14
+ %input{ :type => 'hidden', :name => '_method', :value => 'PUT' }
15
15
  %input{ :type => 'hidden', :name => '_snowman', :value => '☃' }
16
16
 
17
17
  %table.details
@@ -31,6 +31,11 @@
31
31
  last modified
32
32
  %td
33
33
  = Time.parse(@workitem.h.put_at).localtime.strftime('%Y/%m/%d %H:%M:%S')
34
+ %tr
35
+ %td
36
+ wf name / revision
37
+ %td
38
+ = "#{@workitem.wf_name} / #{@workitem.wf_revision}"
34
39
 
35
40
  %tr
36
41
  %td.no_border{ :colspan => 2 }
@@ -85,9 +90,15 @@
85
90
 
86
91
  :javascript
87
92
 
88
- var proc_rep = #{process_tree(@workitem)};
93
+ $(document).ready(function() {
94
+
95
+ Rk.fitFluo($('table.details'));
96
+
97
+ var proc_rep = #{process_tree(@workitem)};
98
+
99
+ RuoteFluo.render('#fluo', proc_rep, { 'fit' : 'down' });
100
+ RuoteFluo.pin('#fluo', '#{@workitem.fei.expid}', 'workitem');
89
101
 
90
- Fluo.renderFlow('fluo', proc_rep, { 'workitems' : ['#{@workitem.fei.expid}'] });
91
- Fluo.crop('fluo');
92
- Fluo.resizeForMaxWidth('fluo', $('body').width() / 2.1);
102
+ Rk.onClickZoom('#fluo');
103
+ });
93
104
 
@@ -1,10 +1,11 @@
1
1
  # encoding: utf-8
2
+ $:.push File.expand_path('../lib', __FILE__)
3
+ require 'ruote-kit/version'
2
4
 
3
5
  Gem::Specification.new do |s|
4
6
 
5
7
  s.name = 'ruote-kit'
6
- #s.version = File.read('lib/ruote-kit/version.rb').match(/VERSION = '([^']+)'/)[1]
7
- s.version = '2.2.0.3'
8
+ s.version = RuoteKit::VERSION
8
9
  s.platform = Gem::Platform::RUBY
9
10
  s.authors = [ 'Kenneth Kalmer', 'Torsten Schoenebaum', 'John Mettraux' ]
10
11
  s.email = [ 'kenneth.kalmer@gmail.com' ]
@@ -13,7 +14,7 @@ Gem::Specification.new do |s|
13
14
  s.summary = 'ruote workflow engine, wrapped in a loving rack embrace'
14
15
  s.description = %q{
15
16
  ruote workflow engine, wrapped in a loving rack embrace
16
- }
17
+ }
17
18
 
18
19
  #s.files = `git ls-files`.split("\n")
19
20
  s.files = Dir[
@@ -23,16 +24,17 @@ ruote workflow engine, wrapped in a loving rack embrace
23
24
  'lib/ruote-kit/public/**/*', 'lib/ruote-kit/views/**/*'
24
25
  ] - [ 'rails-template.rb' ]
25
26
 
26
- s.add_runtime_dependency 'sinatra', '1.0'
27
- s.add_runtime_dependency 'sinatra-respond_to', '0.5.0'
28
- s.add_runtime_dependency 'haml', '3.0.25'
27
+ s.add_runtime_dependency 'sinatra', '>= 1.2.0'
28
+ s.add_runtime_dependency 'sinatra-respond_to', '>= 0.8.0'
29
+
30
+ s.add_runtime_dependency 'haml', '>= 3.1.4'
29
31
 
30
32
  s.add_runtime_dependency 'rufus-json', '>= 0.2.5'
31
33
  #s.add_runtime_dependency('ruote', s.version) unless defined?(Bundler)
32
34
  s.add_runtime_dependency('ruote', '>= 2.2.0') unless defined?(Bundler)
33
35
  # see Gemfile
34
36
 
35
- s.add_development_dependency 'rspec', '2.4.0'
37
+ s.add_development_dependency 'rspec', '>= 2.5.0'
36
38
  s.add_development_dependency 'rack-test', '0.5.7'
37
39
  s.add_development_dependency 'webrat', '0.7.3'
38
40