knjtasks 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +66 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +19 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/files/database_schema.rb +174 -0
- data/lib/knjtasks.rb +172 -0
- data/locales/da_DK/LC_MESSAGES/default.mo +0 -0
- data/locales/da_DK/LC_MESSAGES/default.po +1153 -0
- data/locales/da_DK/title.txt +1 -0
- data/locales/en_GB/title.txt +1 -0
- data/models/class_comment.rb +44 -0
- data/models/class_customer.rb +13 -0
- data/models/class_email_check.rb +7 -0
- data/models/class_project.rb +22 -0
- data/models/class_task.rb +163 -0
- data/models/class_task_assigned_user.rb +62 -0
- data/models/class_task_check.rb +26 -0
- data/models/class_timelog.rb +82 -0
- data/models/class_user.rb +125 -0
- data/models/class_user_project_link.rb +66 -0
- data/models/class_user_rank.rb +3 -0
- data/models/class_user_rank_link.rb +17 -0
- data/models/class_user_task_list_link.rb +17 -0
- data/pages/admin.rhtml +7 -0
- data/pages/comment_edit.rhtml +121 -0
- data/pages/comment_update_id_per_obj.rhtml +41 -0
- data/pages/customer_edit.rhtml +69 -0
- data/pages/customer_search.rhtml +80 -0
- data/pages/customer_show.rhtml +50 -0
- data/pages/frontpage.rhtml +198 -0
- data/pages/project_edit.rhtml +129 -0
- data/pages/project_search.rhtml +82 -0
- data/pages/project_show.rhtml +203 -0
- data/pages/task_check_edit.rhtml +98 -0
- data/pages/task_edit.rhtml +168 -0
- data/pages/task_search.rhtml +131 -0
- data/pages/task_show.rhtml +454 -0
- data/pages/timelog_edit.rhtml +134 -0
- data/pages/timelog_search.rhtml +318 -0
- data/pages/user_edit.rhtml +223 -0
- data/pages/user_login.rhtml +83 -0
- data/pages/user_profile.rhtml +89 -0
- data/pages/user_rank_search.rhtml +95 -0
- data/pages/user_search.rhtml +136 -0
- data/pages/user_show.rhtml +87 -0
- data/pages/workstatus.rhtml +320 -0
- data/scripts/fckeditor_validate_login.rb +23 -0
- data/spec/knjtasks_spec.rb +115 -0
- data/spec/spec_helper.rb +12 -0
- data/threads/thread_mail_task_comments.rb +114 -0
- data/www/api/task.rhtml +9 -0
- data/www/api/user.rhtml +20 -0
- data/www/clean.rhtml +14 -0
- data/www/css/default.css +186 -0
- data/www/gfx/body_bg.jpg +0 -0
- data/www/gfx/button_bg.png +0 -0
- data/www/gfx/main_box_design.png +0 -0
- data/www/gfx/main_box_left.png +0 -0
- data/www/gfx/main_box_right.png +0 -0
- data/www/gfx/main_box_top.png +0 -0
- data/www/gfx/main_box_top_left.png +0 -0
- data/www/gfx/main_box_top_right.png +0 -0
- data/www/index.rhtml +154 -0
- data/www/js/default.js +112 -0
- metadata +208 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
args = {
|
2
|
+
"host" => $validate_login["_SERVER"]["HTTP_HOST"]
|
3
|
+
}
|
4
|
+
|
5
|
+
if $validate_login["_SERVER"]["HTTPS"] or $validate_login["_SERVER"]["HTTP_SSL_ENABLED"]
|
6
|
+
args["ssl"] = true
|
7
|
+
args["port"] = 443
|
8
|
+
args["validate"] = false
|
9
|
+
end
|
10
|
+
|
11
|
+
http = Knj::Http.new(args)
|
12
|
+
http.cookies["KnjappserverSession"] = $validate_login["_COOKIE"]["KnjappserverSession"]
|
13
|
+
data = http.get("/?show=users_login")
|
14
|
+
|
15
|
+
if data["data"].to_s.index("<form method=\"post\" action=\"?show=user_login") == nil
|
16
|
+
print JSON.generate(
|
17
|
+
"Enabled" => true
|
18
|
+
)
|
19
|
+
else
|
20
|
+
print JSON.generate(
|
21
|
+
"Enabled" => false
|
22
|
+
)
|
23
|
+
end
|
@@ -0,0 +1,115 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Knjtasks" do
|
4
|
+
it "should be able to require the needed frameworks and gems" do
|
5
|
+
require "rubygems"
|
6
|
+
require "sqlite3"
|
7
|
+
require "knjrbfw"
|
8
|
+
require "knjtasks"
|
9
|
+
Knj.gem_require(:Hayabusa)
|
10
|
+
Knj.gem_require(:Http2)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should be able to require all the model-framework-files." do
|
14
|
+
models_path = "#{File.dirname(__FILE__)}/../models"
|
15
|
+
Dir.new(models_path).each do |file|
|
16
|
+
next if file == "." or file == ".."
|
17
|
+
fp = "#{models_path}/#{file}"
|
18
|
+
require fp
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should be able to generate a sample database and start a test-environment." do
|
23
|
+
db_path = "#{Knj::Os.tmpdir}/knjtasks_spec_database_sample.sqlite3"
|
24
|
+
File.unlink(db_path) if File.exists?(db_path)
|
25
|
+
|
26
|
+
db = Knj::Db.new(
|
27
|
+
:type => "sqlite3",
|
28
|
+
:path => db_path,
|
29
|
+
:return_keys => "symbols",
|
30
|
+
:index_append_table_name => true
|
31
|
+
)
|
32
|
+
tasks = Knjtasks.new(
|
33
|
+
:title => "rake_rspec",
|
34
|
+
:host => "0.0.0.0",
|
35
|
+
:port => 1515,
|
36
|
+
:db => db,
|
37
|
+
:knjjs_url => "http://www.kaspernj.org/js",
|
38
|
+
:email_admin => "k@spernj.org",
|
39
|
+
:email_robot => "k@spernj.org",
|
40
|
+
:smtp_args => {
|
41
|
+
"smtp_host" => "localhost",
|
42
|
+
"smtp_port" => 25
|
43
|
+
},
|
44
|
+
:db_args => false
|
45
|
+
)
|
46
|
+
|
47
|
+
tasks.start
|
48
|
+
$tasks = tasks
|
49
|
+
|
50
|
+
#The admin-user should exists.
|
51
|
+
user = tasks.ob.get_by(:User, "username" => "admin", "passwd" => Digest::MD5.hexdigest("admin"))
|
52
|
+
raise "Expected admin user to exist but it didnt." if !user
|
53
|
+
|
54
|
+
$http = Http2.new(:host => "localhost", :port => 1515, :follow_redirects => false)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should be able to log in" do
|
58
|
+
res = $http.post(:url => "?show=user_login&choice=dologin", :post => {
|
59
|
+
"texuser" => "admin",
|
60
|
+
"texpass_md5" => Digest::MD5.hexdigest("admin"),
|
61
|
+
"cheremember" => "on"
|
62
|
+
})
|
63
|
+
|
64
|
+
res = $http.get("?show=user_login")
|
65
|
+
raise "Expected to be logged in but wasnt according to HTML:\n#{res.body}" if res.body.index("You are logged in as") == nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should be able to add customers" do
|
69
|
+
res = $http.post(:url => "?show=customer_edit&choice=dosave", :post => {
|
70
|
+
"texname" => "Test customer"
|
71
|
+
})
|
72
|
+
|
73
|
+
if match = res.body.match(/location\.href="\?show=customer_edit&customer_id=(\d+)"/)
|
74
|
+
$customer = $tasks.ob.get(:Customer, match[1])
|
75
|
+
else
|
76
|
+
raise "Expected to be redirected to the new customer but wasnt:\n#{res.body}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should be able to add projects" do
|
81
|
+
res = $http.post(:url => "?show=project_edit&choice=dosave", :post => {
|
82
|
+
"texname" => "Test project",
|
83
|
+
"selcustomer" => $customer.id,
|
84
|
+
"texdescr" => "Test project"
|
85
|
+
})
|
86
|
+
|
87
|
+
if match = res.body.match(/location\.href="\?show=project_edit&project_id=(\d+)"/)
|
88
|
+
$project = $tasks.ob.get(:Project, match[1])
|
89
|
+
else
|
90
|
+
raise "Expected to be redirected to new project but wasnt:\n#{res.body}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should be able to add tasks" do
|
95
|
+
res = $http.post(:url => "?show=task_edit&choice=dosave", :post => {
|
96
|
+
"texname" => "Test task",
|
97
|
+
"texdescr" => "Test description",
|
98
|
+
"selproject" => $project.id,
|
99
|
+
"type" => "feature"
|
100
|
+
})
|
101
|
+
|
102
|
+
if match = res.body.match(/location\.href="\?show=task_show&task_id=(\d+)"/)
|
103
|
+
$task = $tasks.ob.get(:Task, match[1])
|
104
|
+
else
|
105
|
+
raise "Expected to be redirected to new task but wasnt:\n#{res.body}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should show special HTML for expired sessions" do
|
110
|
+
res = $http.get(:url => "?show=user_login&choice=dologout")
|
111
|
+
res = $http.get(:url => "clean.rhtml?show=task_show&task_id=#{$task.id}&choice=gettimelogs")
|
112
|
+
|
113
|
+
raise "Expected message about not logged in but got this instead: '#{res.body}'." if res.body != "You are not logged in. Log in and try to view this page again."
|
114
|
+
end
|
115
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'knjtasks'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
#This class checks the mail-account for mails and imports them as comments.
|
2
|
+
class Knjtasks::Thread_mail_task_comments
|
3
|
+
def initialize(args = {})
|
4
|
+
@args = args
|
5
|
+
|
6
|
+
require "rubygems"
|
7
|
+
require "mail"
|
8
|
+
|
9
|
+
if !@args[:args][:mail_args][:port]
|
10
|
+
@args[:args][:mail_args][:port] = 143 if @args[:args][:mail_args][:type] == "imap"
|
11
|
+
@args[:args][:mail_args][:port] = 100 if @args[:args][:mail_args][:type] == "pop3"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def run
|
16
|
+
STDOUT.print "Checking for mails.\n" if @args[:appsrv].debug
|
17
|
+
conn = Net::IMAP.new(@args[:args][:mail_args][:host], @args[:args][:mail_args][:port].to_i, @args[:args][:mail_args][:ssl])
|
18
|
+
|
19
|
+
if @args[:args][:mail_args][:user] and @args[:args][:mail_args][:pass]
|
20
|
+
conn.login(@args[:args][:mail_args][:user], @args[:args][:mail_args][:pass])
|
21
|
+
end
|
22
|
+
|
23
|
+
conn.select("INBOX")
|
24
|
+
emails = conn.search(["ALL"])
|
25
|
+
emails.each do |msg_id|
|
26
|
+
error = nil
|
27
|
+
from = nil
|
28
|
+
html = nil
|
29
|
+
|
30
|
+
begin
|
31
|
+
msg = conn.fetch(msg_id, "(ENVELOPE RFC822)")
|
32
|
+
mail = Mail.new(msg[0].attr["RFC822"])
|
33
|
+
|
34
|
+
env = msg[0].attr["ENVELOPE"]
|
35
|
+
from = "#{env.from.first.mailbox}@#{env.from.first.host}"
|
36
|
+
env_msg_id = env.message_id
|
37
|
+
|
38
|
+
if _ob.static(:Email_check, :checked_id?, env_msg_id)
|
39
|
+
next
|
40
|
+
else
|
41
|
+
_db.insert(:Email_check, {:email_id_str => env_msg_id})
|
42
|
+
end
|
43
|
+
|
44
|
+
user = @args[:ob].get_by(:User, {"email" => from})
|
45
|
+
raise "Could not find a user in this task-system with that email: '#{from}'." if !user
|
46
|
+
|
47
|
+
subj_match = mail.subject.to_s.match(/^([A-z]{1,3}):\s+(\S+)\s+#(\d+):\s+/)
|
48
|
+
raise "Could not figure out the task-ID from the email." if !subj_match
|
49
|
+
task_id = subj_match[3]
|
50
|
+
|
51
|
+
begin
|
52
|
+
task = @args[:ob].get(:Task, task_id)
|
53
|
+
rescue
|
54
|
+
raise "Could not find a task in this task-system with that task-ID: '#{task_id}'."
|
55
|
+
end
|
56
|
+
|
57
|
+
parts = {}
|
58
|
+
mail.parts.each do |part|
|
59
|
+
if part.content_type.match(/^text\/plain/)
|
60
|
+
parts[:plain] = part.body.to_s
|
61
|
+
elsif part.content_type.match(/^text\/html/) and match = part.body.match(/<body.*>([\s\S]+)<\/body>/)
|
62
|
+
parts[:html] = match[1]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
if parts[:html]
|
67
|
+
html = parts[:html]
|
68
|
+
html.gsub!(/<blockquote([\s\S]+?)>([\s\S]+?)<\/blockquote>/i, "")
|
69
|
+
html.gsub!(/on\s+\d+.+?\s+wrote:/i, "")
|
70
|
+
html.gsub!(/<pre.+?class="moz-signature".*?>([\s\S]*?)<\/pre>/i, "")
|
71
|
+
html = Knj::Strings.strip(html, {
|
72
|
+
:strips => [" ", "\n", "\r", "<br>", "<br />", "<br/>"]
|
73
|
+
})
|
74
|
+
elsif parts[:plain]
|
75
|
+
html = parts[:plain]
|
76
|
+
html.gsub!(/on\s+\d+.+?\s+wrote:/i, "")
|
77
|
+
html.gsub!(/^>(.*?)\n/, "")
|
78
|
+
html.gsub!(/\n-- \n[\s\S]++/, "")
|
79
|
+
html = Knj::Strings.strip(html, {
|
80
|
+
:strips => ["\n", "\r", "<br>", "<br />", "<br/>"]
|
81
|
+
})
|
82
|
+
html.gsub!(/\n/, "<br>")
|
83
|
+
end
|
84
|
+
|
85
|
+
raise "Could not read any content in the email." if !html
|
86
|
+
|
87
|
+
comment = @args[:ob].add(:Comment, {
|
88
|
+
:object_class => :Task,
|
89
|
+
:object_id => task.id,
|
90
|
+
:comment => html,
|
91
|
+
:user_id => user.id
|
92
|
+
})
|
93
|
+
|
94
|
+
conn.store(msg_id, "+FLAGS", ["DELETED"])
|
95
|
+
conn.expunge
|
96
|
+
rescue => e
|
97
|
+
if !from
|
98
|
+
STDOUT.print "Could not respond to task-email."
|
99
|
+
STDOUT.puts e.inspect
|
100
|
+
STDOUT.puts e.backtrace
|
101
|
+
else
|
102
|
+
@args[:appsrv].mail(
|
103
|
+
:to => from,
|
104
|
+
:subject => "Your email to the task-system.",
|
105
|
+
:text => "Could not parse your email:\n\n#{e.inspect}\n#{e.backtrace.join("\n")}"
|
106
|
+
)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
conn.logout
|
112
|
+
conn.disconnect
|
113
|
+
end
|
114
|
+
end
|
data/www/api/task.rhtml
ADDED
data/www/api/user.rhtml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
<%
|
2
|
+
if _get["choice"] == "dologin"
|
3
|
+
user = _ob.get_by(:User, {
|
4
|
+
"username" => _post["username"],
|
5
|
+
"passwd" => _post["password"]
|
6
|
+
})
|
7
|
+
if !user
|
8
|
+
print JSON.generate(
|
9
|
+
"result" => false,
|
10
|
+
"msg" => "User not found."
|
11
|
+
)
|
12
|
+
else
|
13
|
+
print JSON.generate(
|
14
|
+
"result" => true,
|
15
|
+
"msg" => "Logged in as user #{user.id}.",
|
16
|
+
"user_data" => user.data
|
17
|
+
)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
%>
|
data/www/clean.rhtml
ADDED
data/www/css/default.css
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
html, body, input, textarea, select, td{
|
2
|
+
font-family: verdana, tahoma, arial;
|
3
|
+
font-size: 12px;
|
4
|
+
color: black;
|
5
|
+
}
|
6
|
+
|
7
|
+
html, body, html > body > center{
|
8
|
+
width: 100%;
|
9
|
+
height: 100%;
|
10
|
+
margin: 0px;
|
11
|
+
padding: 0px;
|
12
|
+
}
|
13
|
+
|
14
|
+
body{
|
15
|
+
background: url("/gfx/body_bg.jpg") 0 0 repeat;
|
16
|
+
}
|
17
|
+
|
18
|
+
a:link, a:visited{
|
19
|
+
color: #698295;
|
20
|
+
text-decoration: none;
|
21
|
+
}
|
22
|
+
|
23
|
+
a:active, a:hover{
|
24
|
+
color: #272727;
|
25
|
+
text-decoration: none;
|
26
|
+
}
|
27
|
+
|
28
|
+
table.main_table{
|
29
|
+
height: 100%;
|
30
|
+
width: 1200px;
|
31
|
+
border-top: none;
|
32
|
+
border-bottom: none;
|
33
|
+
text-align: left;
|
34
|
+
}
|
35
|
+
|
36
|
+
td.main_logo{
|
37
|
+
font-size: 24px;
|
38
|
+
padding: 15px;
|
39
|
+
padding-left: 28px;
|
40
|
+
height: 40px;
|
41
|
+
}
|
42
|
+
|
43
|
+
div.main_menu_outer{
|
44
|
+
position: relative;
|
45
|
+
height: 19px;
|
46
|
+
}
|
47
|
+
|
48
|
+
div.main_menu_inner{
|
49
|
+
position: absolute;
|
50
|
+
top: -15px;
|
51
|
+
font-size: 16px;
|
52
|
+
}
|
53
|
+
|
54
|
+
td.main_bottom{
|
55
|
+
text-align: center;
|
56
|
+
padding: 4px;
|
57
|
+
background-color: #ffffff;
|
58
|
+
height: 25px;
|
59
|
+
}
|
60
|
+
|
61
|
+
td.main_corner_top_left{
|
62
|
+
width: 41px;
|
63
|
+
height: 41px;
|
64
|
+
background: url("/gfx/main_box_top_left.png") 0 0 no-repeat;
|
65
|
+
}
|
66
|
+
|
67
|
+
td.main_corner_top_right{
|
68
|
+
width: 41px;
|
69
|
+
height: 41px;
|
70
|
+
background: url("/gfx/main_box_top_right.png") 0 0 no-repeat;
|
71
|
+
}
|
72
|
+
|
73
|
+
td.main_side_top{
|
74
|
+
height: 41px;
|
75
|
+
background: url("/gfx/main_box_top.png") 0 0 repeat-x;
|
76
|
+
}
|
77
|
+
|
78
|
+
td.main_side_left{
|
79
|
+
width: 41px;
|
80
|
+
background: url("/gfx/main_box_left.png") 0 0 repeat-y;
|
81
|
+
}
|
82
|
+
|
83
|
+
td.main_side_right{
|
84
|
+
width: 41px;
|
85
|
+
background: url("/gfx/main_box_right.png") 0 0 repeat-y;
|
86
|
+
}
|
87
|
+
|
88
|
+
td.main_body{
|
89
|
+
background-color: #ffffff;
|
90
|
+
vertical-align: top;
|
91
|
+
}
|
92
|
+
|
93
|
+
table.box{
|
94
|
+
border: none;
|
95
|
+
}
|
96
|
+
|
97
|
+
td.box_header{
|
98
|
+
font-weight: bold;
|
99
|
+
white-space: nowrap;
|
100
|
+
text-align: right;
|
101
|
+
background-color: #666666;
|
102
|
+
padding: 5px;
|
103
|
+
color: #d9d9d9;
|
104
|
+
}
|
105
|
+
|
106
|
+
td.box_header_spacer{
|
107
|
+
width: 27%;
|
108
|
+
}
|
109
|
+
|
110
|
+
td.box_content{
|
111
|
+
border: 1px solid #666666;
|
112
|
+
padding: 7px;
|
113
|
+
padding-right: 14px;
|
114
|
+
background-color: #ffffff;
|
115
|
+
}
|
116
|
+
|
117
|
+
.buttons{
|
118
|
+
text-align: right;
|
119
|
+
}
|
120
|
+
|
121
|
+
table.form, table.list{
|
122
|
+
width: 100%;
|
123
|
+
}
|
124
|
+
|
125
|
+
thead > tr > th{
|
126
|
+
font-size: 12px;
|
127
|
+
text-align: left;
|
128
|
+
white-space: nowrap;
|
129
|
+
}
|
130
|
+
|
131
|
+
td.tdt, td.tdcheck{
|
132
|
+
font-weight: bold;
|
133
|
+
white-space: nowrap;
|
134
|
+
}
|
135
|
+
|
136
|
+
td.tdc{
|
137
|
+
width: 100%;
|
138
|
+
}
|
139
|
+
|
140
|
+
td.tdd, div.tdd{
|
141
|
+
color: grey;
|
142
|
+
padding-top: 0px;
|
143
|
+
padding-bottom: 7px;
|
144
|
+
font-size: 10px;
|
145
|
+
}
|
146
|
+
|
147
|
+
td.tdr, th.tdr{
|
148
|
+
text-align: right;
|
149
|
+
}
|
150
|
+
|
151
|
+
.error{
|
152
|
+
font-style: italic;
|
153
|
+
}
|
154
|
+
|
155
|
+
td.error{
|
156
|
+
text-align: center;
|
157
|
+
}
|
158
|
+
|
159
|
+
input, select, textarea{
|
160
|
+
border: 1px solid #bbbbbb;
|
161
|
+
background-color: #ffffff;
|
162
|
+
color: #000000;
|
163
|
+
padding: 4px;
|
164
|
+
}
|
165
|
+
|
166
|
+
input[type=button], input[type=submit], input.button{
|
167
|
+
border: 1px solid #989898;
|
168
|
+
background: url("/gfx/button_bg.png") 0 0 repeat-x;
|
169
|
+
height: 20px;
|
170
|
+
font-size: 12px;
|
171
|
+
padding: 0px 9px 0px 9px;
|
172
|
+
margin: 0px 0px 0px 0px;
|
173
|
+
}
|
174
|
+
|
175
|
+
input[type=checkbox]{
|
176
|
+
border: none;
|
177
|
+
background-color: transparent;
|
178
|
+
}
|
179
|
+
|
180
|
+
input.input_text, input.input_password, select.input_select, textarea.input_textarea{
|
181
|
+
width: 100%;
|
182
|
+
}
|
183
|
+
|
184
|
+
.nowrap{
|
185
|
+
white-space: nowrap;
|
186
|
+
}
|