rails-caddy 0.0.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +69 -0
- data/VERSION.yml +4 -0
- data/lib/rails-caddy.rb +43 -0
- data/lib/rails-caddy/controllers/rails_caddy_controller.rb +17 -0
- data/lib/rails-caddy/controllers/sanitize_email_controller.rb +42 -0
- data/lib/rails-caddy/controllers/session_editing_controller.rb +21 -0
- data/lib/rails-caddy/controllers/timecop_controller.rb +56 -0
- data/lib/rails-caddy/errors.rb +8 -0
- data/lib/rails-caddy/helpers/rails_caddy_helper.rb +33 -0
- data/lib/rails-caddy/session_controller_finder.rb +17 -0
- data/lib/rails-caddy/views/_rails_caddy.html.erb +57 -0
- data/lib/rails-caddy/views/_rails_caddy_css.html.erb +60 -0
- data/lib/rails-caddy/views/_rails_caddy_js.html.erb +88 -0
- data/test/README.txt +40 -0
- data/test/files/acet.rb +40 -0
- data/test/files/fct.rb +35 -0
- data/test/files/rcct.rb +13 -0
- data/test/files/sanitize_email_action_controller_extensions_test_methods.rb +71 -0
- data/test/files/sanitize_email_controller_test_methods.rb +31 -0
- data/test/files/session_editing_controller_test_methods.rb +41 -0
- data/test/files/timecop_action_controller_extensions_test_methods.rb +55 -0
- data/test/files/timecop_controller_test_methods.rb +30 -0
- data/test/geminstaller.yml +5 -0
- data/test/rails_caddy_controller_test.rb +43 -0
- data/test/rails_caddy_helper_test.rb +24 -0
- data/test/rails_caddy_test.rb +39 -0
- data/test/rails_modifier.rb +131 -0
- data/test/routes.rb +3 -0
- data/test/session_controller_finder_test.rb +57 -0
- data/test/test_helper.rb +12 -0
- metadata +110 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 John Trupiano
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
= rails-caddy
|
2
|
+
|
3
|
+
A developer's QA "caddy" that aids in QA'ing, debugging, and otherwise navigating your application during development and/or QA.
|
4
|
+
|
5
|
+
== Rails Compatibility
|
6
|
+
|
7
|
+
Tests cover rails 2.1.2, 2.2.2, and 2.3.2 specifically. Previous releases on each minor version have not been explicitly tested, but I suspect they should all function equally well.
|
8
|
+
|
9
|
+
== Usage
|
10
|
+
|
11
|
+
RULE #1: DO NOT DEPLOY THIS TO PRODUCTION. I WILL HAVE NO EMPATHY WHATSOEVER IF YOU DO NOT HEED MY WARNING. THIS IS A VERY DANGEROUS GEM THAT WILL DEFINITELY SCREW YOU OVER IF YOU DEPLOY IT TO PRODUCTION.
|
12
|
+
|
13
|
+
This process is not currently scripted, but following these steps will get you off and running.
|
14
|
+
|
15
|
+
* edit application.rb
|
16
|
+
|
17
|
+
if Object.const_defined?(:RailsCaddy)
|
18
|
+
helper RailsCaddyHelper
|
19
|
+
around_filter :handle_sanitize_email
|
20
|
+
around_filter :handle_timecop_offset, :except => [:timecop_update, :timecop_reset]
|
21
|
+
end
|
22
|
+
|
23
|
+
* edit config/environments/development.rb -- DO NOT MAKE THIS AVAILABLE TO PRODUCTION!!!
|
24
|
+
|
25
|
+
config.gem "rails-caddy"
|
26
|
+
|
27
|
+
config.after_initialize do
|
28
|
+
require 'rails-caddy'
|
29
|
+
require_dependency 'application_controller' # 'application' if pre rails 2.3
|
30
|
+
RailsCaddy.init!
|
31
|
+
|
32
|
+
ActionMailer::Base.sanitized_recipients = "nobody@smartlogicsolutions.com"
|
33
|
+
end
|
34
|
+
|
35
|
+
* add just before you close your body tag in your layout (it's actually unimportant where you place it, as long as it's in the body):
|
36
|
+
|
37
|
+
<%= rails_caddy if Object.const_defined?(:RailsCaddy) %>
|
38
|
+
|
39
|
+
* add to the top of config/routes.rb
|
40
|
+
|
41
|
+
RailsCaddy.define_routes!(map) if Object.const_defined?(:RailsCaddy)
|
42
|
+
|
43
|
+
== Dependencies
|
44
|
+
|
45
|
+
rails-caddy is dependent on the {sanitize_email gem}[http://github.com/jtrupiano/sanitize_email/tree/master]. Unfortunately, for the time being you'll need to build and install that locally. Why? Because the dependency is on +sanitize_email+ and NOT <tt>jtrupiano-sanitize_email</tt>, the latter of which can be installed remotely. Hopefully this will change soon (that's _your_ cue to fork and fix).
|
46
|
+
|
47
|
+
== Building/Testing
|
48
|
+
|
49
|
+
In order to run the tests, you'll want to build the gem. Why? Because <tt>rake test:rails_compatibility</tt> tests all supported versions of rails (see <b>Rails Compatibility</b>, above). It does this by creating mini-Rails apps for each version and config-gem'ing rails-caddy (<tt>config.gem 'rails-caddy'</tt>). If you don't build the gem and try to run the tests, you'll get an error telling you to <tt>run `rake gems:install`</tt>.
|
50
|
+
|
51
|
+
The easiest way to build the gem is to install {technicalpickle's}[http://technicalpickles.com/] {jeweler gem}[http://github.com/technicalpickles/jeweler/tree/master]: <tt>sudo gem install jeweler</tt>.
|
52
|
+
|
53
|
+
After jeweler is installed, you can build and install with some handy rake tasks:
|
54
|
+
|
55
|
+
rake build
|
56
|
+
rake install
|
57
|
+
|
58
|
+
<b>n.b.</b> <tt>rake install</tt> uses +sudo+
|
59
|
+
|
60
|
+
(Problems? See *Dependencies*, above.)
|
61
|
+
|
62
|
+
Now you can run your tests:
|
63
|
+
|
64
|
+
rake test:all # Run all test suites.
|
65
|
+
rake test:rails_compatibility # Test all supported versions of rails.
|
66
|
+
|
67
|
+
== Copyright
|
68
|
+
|
69
|
+
Copyright (c) 2009 John Trupiano. See LICENSE for details.
|
data/VERSION.yml
ADDED
data/lib/rails-caddy.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'timecop'
|
2
|
+
require 'actionmailer' # would like to remove this
|
3
|
+
require 'sanitize_email'
|
4
|
+
require 'rails-caddy/errors'
|
5
|
+
require 'rails-caddy/controllers/session_editing_controller'
|
6
|
+
require 'rails-caddy/controllers/timecop_controller'
|
7
|
+
require 'rails-caddy/controllers/sanitize_email_controller'
|
8
|
+
require 'rails-caddy/helpers/rails_caddy_helper'
|
9
|
+
|
10
|
+
$rails_caddy_activated = false
|
11
|
+
|
12
|
+
class RailsCaddy
|
13
|
+
|
14
|
+
def self.init!
|
15
|
+
# extend ActionController::Base
|
16
|
+
ActionController::Base.send(:include, TimecopController::ActionControllerExtensions)
|
17
|
+
ActionController::Base.send(:include, SanitizeEmailController::ActionControllerExtensions)
|
18
|
+
|
19
|
+
# Pull in the RailsCaddyController
|
20
|
+
require 'rails-caddy/controllers/rails_caddy_controller'
|
21
|
+
|
22
|
+
# Lastly, let's add our views to the load path...
|
23
|
+
ActionController::Base.append_view_path(File.expand_path(File.join(File.dirname(__FILE__), "rails-caddy", "views")))
|
24
|
+
|
25
|
+
# we will inspect this at a few places in the consuming rails app, most notably config/routes.rb
|
26
|
+
$rails_caddy_activated = true
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.define_routes!(map)
|
30
|
+
# Session editing routes
|
31
|
+
map.update_session '/rails_caddy/update_session/:id', :controller => 'rails_caddy', :action => 'update_session'
|
32
|
+
map.remove_session '/rails_caddy/remove_session/:id', :controller => 'rails_caddy', :action => 'remove_session'
|
33
|
+
|
34
|
+
# Timecop routes
|
35
|
+
map.timecop_update '/rails_caddy/timecop_update', :controller => 'rails_caddy', :action => 'timecop_update'
|
36
|
+
map.timecop_reset '/rails_caddy/timecop_reset', :controller => 'rails_caddy', :action => 'timecop_reset'
|
37
|
+
|
38
|
+
# Sanitize Email routes
|
39
|
+
map.set_sanitize_email_address '/rails_caddy/set_sanitize_email_address', :controller => 'rails_caddy', :action => 'set_sanitize_email_address'
|
40
|
+
map.unset_sanitize_email_address '/rails_caddy/unset_sanitize_email_address', :controller => 'rails_caddy', :action => 'unset_sanitize_email_address'
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'rails-caddy/session_controller_finder'
|
2
|
+
|
3
|
+
# Find the controller responsible for establishing session
|
4
|
+
session_controller = SessionControllerFinder.find
|
5
|
+
|
6
|
+
# Dynamically define the RailsCaddyController now
|
7
|
+
c = Class.new(session_controller) do
|
8
|
+
include SessionEditingController
|
9
|
+
include TimecopController
|
10
|
+
include SanitizeEmailController
|
11
|
+
|
12
|
+
def verify_authenticity_token
|
13
|
+
# this is lame that I have to override it here...can't figure out why a skip_before_filter fails
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
Object.const_set("RailsCaddyController", c)
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module SanitizeEmailController
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
base.send(:include, Actions)
|
5
|
+
end
|
6
|
+
|
7
|
+
module Actions
|
8
|
+
|
9
|
+
def set_sanitize_email_address
|
10
|
+
session[:sanitize_email_address] = params[:value]
|
11
|
+
render :status => 200, :text => params[:value]
|
12
|
+
end
|
13
|
+
|
14
|
+
def unset_sanitize_email_address
|
15
|
+
session[:sanitize_email_address] = nil
|
16
|
+
render :status => 200, :text => "nil"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ActionControllerExtensions
|
21
|
+
# def self.included(base)
|
22
|
+
# base.class_eval do
|
23
|
+
# around_filter :handle_sanitize_email
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
|
27
|
+
def handle_sanitize_email
|
28
|
+
if session[:sanitize_email_address].nil?
|
29
|
+
yield
|
30
|
+
return
|
31
|
+
end
|
32
|
+
|
33
|
+
@original_sanitized_recipients = ActionMailer::Base.sanitized_recipients
|
34
|
+
ActionMailer::Base.sanitized_recipients = session[:sanitize_email_address]
|
35
|
+
yield
|
36
|
+
ensure
|
37
|
+
ActionMailer::Base.sanitized_recipients = @original_sanitized_recipients
|
38
|
+
end
|
39
|
+
|
40
|
+
private :handle_sanitize_email
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module SessionEditingController
|
2
|
+
|
3
|
+
def update_session
|
4
|
+
if params[:id].nil?
|
5
|
+
render :status => 422, :text => "Invalid request. No session variable provided."
|
6
|
+
return false
|
7
|
+
end
|
8
|
+
session[params[:id].to_sym] = params[:value]
|
9
|
+
render :status => 200, :text => params[:value]
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove_session
|
13
|
+
if params[:id].nil?
|
14
|
+
render :status => 422, :text => "Invalid request. Session variable is either missing or invalid."
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
session[params[:id].to_sym] = nil
|
18
|
+
render :status => 200, :nothing => true
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module TimecopController
|
2
|
+
|
3
|
+
def self.included(base)
|
4
|
+
# base.class_eval do
|
5
|
+
# # Make sure we don't reset our own time!
|
6
|
+
# skip_filter :handle_timecop_offset
|
7
|
+
# end
|
8
|
+
base.send(:include, Actions)
|
9
|
+
end
|
10
|
+
|
11
|
+
module Actions
|
12
|
+
def timecop_update
|
13
|
+
year, month, day, hour, min, sec = params[:year], params[:month], params[:day], params[:hour], params[:min], params[:sec]
|
14
|
+
session[:timecop_adjusted_time] = Time.local(year, month, day, hour, min, sec)
|
15
|
+
render :status => 200, :nothing => true
|
16
|
+
end
|
17
|
+
|
18
|
+
def timecop_reset
|
19
|
+
session[:timecop_adjusted_time] = nil
|
20
|
+
render :status => 200, :nothing => true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ActionControllerExtensions
|
25
|
+
# def self.included(base)
|
26
|
+
# base.class_eval do
|
27
|
+
# around_filter :handle_timecop_offset
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
|
31
|
+
# to be used as an around_filter
|
32
|
+
def handle_timecop_offset
|
33
|
+
# Establish now
|
34
|
+
if !session[:timecop_adjusted_time].nil?
|
35
|
+
#puts "***** Time traveling to #{session[:timecop_adjusted_time].to_s}"
|
36
|
+
Timecop.travel(session[:timecop_adjusted_time])
|
37
|
+
else
|
38
|
+
Timecop.return
|
39
|
+
end
|
40
|
+
|
41
|
+
# Run the intended action
|
42
|
+
yield
|
43
|
+
|
44
|
+
# we want to continue to slide time forward, even if it's only 3 seconds at a time.
|
45
|
+
# this ensures that subsequent calls during the same "time travel" actually pass time
|
46
|
+
if !session[:timecop_adjusted_time].nil?
|
47
|
+
#puts "====== Resetting session to: #{Time.now + 3}"
|
48
|
+
session[:timecop_adjusted_time] = Time.now + 3 # slide it forward a couple of seconds
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private :handle_timecop_offset
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module RailsCaddyHelper
|
2
|
+
# actually embeds the magic that is the Rails Caddy
|
3
|
+
def rails_caddy
|
4
|
+
render(:partial => '/rails_caddy')
|
5
|
+
end
|
6
|
+
|
7
|
+
def translated_remove_session_path
|
8
|
+
url = remove_session_path
|
9
|
+
url << "/" unless url.match(/\/$/)
|
10
|
+
end
|
11
|
+
|
12
|
+
def rc_in_place_editor(key, field_id, url)
|
13
|
+
function = "RailsCaddy.editors['" + key + "'] = new Ajax.InPlaceEditor("
|
14
|
+
function << "'#{field_id}', "
|
15
|
+
function << "'#{url}'"
|
16
|
+
function << ');'
|
17
|
+
|
18
|
+
javascript_tag(function)
|
19
|
+
end
|
20
|
+
|
21
|
+
def rc_in_place_editor_field(key, value)
|
22
|
+
tag = ::ActionView::Helpers::InstanceTag.new("rails_caddy", key, self)
|
23
|
+
tag_options = {:tag => "span", :id => "rails_caddy_#{key}_in_place_editor", :name => "value", :class => "in_place_editor_field"}
|
24
|
+
|
25
|
+
# rails < 2.3 needs to be treated with kid gloves.
|
26
|
+
url = update_session_path
|
27
|
+
url << "/" unless url.match(/\/$/)
|
28
|
+
url << key
|
29
|
+
|
30
|
+
tag.content_tag(tag_options.delete(:tag), value, tag_options) + rc_in_place_editor(key, tag_options[:id], url)
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Responsible for locating the controller responsible for establishing the session
|
2
|
+
class SessionControllerFinder
|
3
|
+
def self.find
|
4
|
+
if !Object.const_defined?(:ApplicationController)
|
5
|
+
raise RailsCaddy::SessionControllerNotFoundError,
|
6
|
+
"Cannot find ApplicationController. If you're sure that you have defined it, try adding require_dependency 'application_controller' prior to invoking RailsCaddy.init!"
|
7
|
+
end
|
8
|
+
|
9
|
+
candidate = ApplicationController
|
10
|
+
# if candidate.session_options[:key].nil?
|
11
|
+
# raise RailsCaddy::SessionUninitializedError,
|
12
|
+
# "session does not appear to be established for #{candidate.class}. session: #{candidate.session_options.inspect}"
|
13
|
+
# end
|
14
|
+
|
15
|
+
candidate
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
<%= render(:partial => '/rails_caddy_css') %>
|
2
|
+
|
3
|
+
<%= render(:partial => '/rails_caddy_js')%>
|
4
|
+
|
5
|
+
<div id="railsCaddy">
|
6
|
+
|
7
|
+
<a href="#" id="railsCaddyTab">caddy</a>
|
8
|
+
|
9
|
+
<div id="railsCaddyContents" style="display: none;">
|
10
|
+
<div id="railsCaddyContentsInner">
|
11
|
+
<div id="railsCaddySession">
|
12
|
+
<h2>Session</h2>
|
13
|
+
<div id="sessionObjects">
|
14
|
+
<script type="text/javascript">
|
15
|
+
<% (session.data.keys - ["flash", :timecop_adjusted_time, :sanitize_email_address]).each do |key| -%>
|
16
|
+
<% next if session[key].nil? -%>
|
17
|
+
document.write(RailsCaddy.sessionVariableEditor("<%= escape_javascript(key.to_s) %>", "<%= escape_javascript(session[key].to_s) %>"));
|
18
|
+
<% end -%>
|
19
|
+
</script>
|
20
|
+
</div>
|
21
|
+
<p><a href="javascript:void(0)" onclick="RailsCaddy.addSessionVariable();">+ New Session Variable</a></p>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div id="railsCaddyTimecop">
|
25
|
+
<h2>Timecop</h2>
|
26
|
+
<p>The time is <strong style="color: red;"><%= Time.now.to_s(:db) %></strong></p>
|
27
|
+
<% form_remote_tag :url => timecop_update_path do -%>
|
28
|
+
<% %w(year month day hour min sec).each do |field| -%>
|
29
|
+
<%= text_field_tag field, Time.now.send(field), :size => (field == "year" ? 3 : 1) %>
|
30
|
+
<% end -%>
|
31
|
+
<p>
|
32
|
+
<%= submit_tag "Time Travel" %>
|
33
|
+
<%= link_to_remote "Reset", :url => timecop_reset_path %>
|
34
|
+
</p>
|
35
|
+
<% end -%>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<div id="railsCaddyEmail">
|
39
|
+
<h2>Sanitize Email</h2>
|
40
|
+
<p>All email sent to: <strong style="color: red;"><%= ActionMailer::Base.sanitized_recipients %></strong></p>
|
41
|
+
<% form_remote_tag :url => set_sanitize_email_address_path do -%>
|
42
|
+
<%= text_field_tag "value", "", :size => 15 %>
|
43
|
+
<p>
|
44
|
+
<%= submit_tag "Change Email" %>
|
45
|
+
<%= link_to_remote "Unset", :url => unset_sanitize_email_address_path %>
|
46
|
+
</p>
|
47
|
+
<% end -%>
|
48
|
+
</div>
|
49
|
+
|
50
|
+
<div id="railsCaddyStats">
|
51
|
+
<h2>Stats</h2>
|
52
|
+
</div>
|
53
|
+
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
|
57
|
+
</div>
|
@@ -0,0 +1,60 @@
|
|
1
|
+
<style type="text/css">
|
2
|
+
#railsCaddy {
|
3
|
+
text-align: left;
|
4
|
+
position: absolute;
|
5
|
+
width: auto;
|
6
|
+
height: auto;
|
7
|
+
top: 100px;
|
8
|
+
left: 0px;
|
9
|
+
z-index: 100;
|
10
|
+
font-family: Helvetica, Arial, Times;
|
11
|
+
}
|
12
|
+
|
13
|
+
#railsCaddyTab {
|
14
|
+
float: left;
|
15
|
+
height: 137px;
|
16
|
+
width: 28px;
|
17
|
+
text-decoration: none;
|
18
|
+
}
|
19
|
+
|
20
|
+
#railsCaddyTab img {
|
21
|
+
border: none;
|
22
|
+
}
|
23
|
+
|
24
|
+
#railsCaddyContents {
|
25
|
+
float: left;
|
26
|
+
overflow: hidden !important;
|
27
|
+
width: 200px;
|
28
|
+
}
|
29
|
+
|
30
|
+
#railsCaddyContentsInner {
|
31
|
+
width: 200px;
|
32
|
+
margin-top: 30px;
|
33
|
+
font-size: 12px;
|
34
|
+
}
|
35
|
+
|
36
|
+
#railsCaddyContentsInner > div {
|
37
|
+
border: solid 1px red;
|
38
|
+
}
|
39
|
+
|
40
|
+
#railsCaddyContentsInner h2 {
|
41
|
+
margin: 0px 0px 3px 0px;
|
42
|
+
padding: 2px;
|
43
|
+
background-color: #CC3400;
|
44
|
+
color: #FFFFFF;
|
45
|
+
font-size: 14px;
|
46
|
+
}
|
47
|
+
|
48
|
+
#railsCaddySession p {
|
49
|
+
margin-top: 0px;
|
50
|
+
margin-bottom: 4px;
|
51
|
+
}
|
52
|
+
|
53
|
+
#railsCaddySession p strong {
|
54
|
+
font-size: 110%;
|
55
|
+
}
|
56
|
+
|
57
|
+
#railsCaddySession a.x {
|
58
|
+
color: red;
|
59
|
+
}
|
60
|
+
</style>
|