watch_tower 0.0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (115) hide show
  1. data/.gitignore +10 -0
  2. data/.todo +33 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +43 -0
  5. data/Guardfile +25 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +38 -0
  8. data/Rakefile +8 -0
  9. data/TODO +17 -0
  10. data/bin/watchtower +10 -0
  11. data/ci/adapters/jruby-mysql.yml +8 -0
  12. data/ci/adapters/jruby-postgresql.yml +6 -0
  13. data/ci/adapters/jruby-sqlite.yml +6 -0
  14. data/ci/adapters/ruby-mysql.yml +8 -0
  15. data/ci/adapters/ruby-postgresql.yml +6 -0
  16. data/ci/adapters/ruby-sqlite.yml +6 -0
  17. data/ci/travis.rb +102 -0
  18. data/lib/watch_tower.rb +60 -0
  19. data/lib/watch_tower/appscript.rb +22 -0
  20. data/lib/watch_tower/cli.rb +15 -0
  21. data/lib/watch_tower/cli/.gitkeep +0 -0
  22. data/lib/watch_tower/cli/install.rb +63 -0
  23. data/lib/watch_tower/cli/open.rb +24 -0
  24. data/lib/watch_tower/cli/start.rb +140 -0
  25. data/lib/watch_tower/config.rb +38 -0
  26. data/lib/watch_tower/core_ext.rb +4 -0
  27. data/lib/watch_tower/core_ext/.gitkeep +0 -0
  28. data/lib/watch_tower/editor.rb +17 -0
  29. data/lib/watch_tower/editor/.gitkeep +0 -0
  30. data/lib/watch_tower/editor/base_appscript.rb +34 -0
  31. data/lib/watch_tower/editor/base_ps.rb +6 -0
  32. data/lib/watch_tower/editor/textmate.rb +17 -0
  33. data/lib/watch_tower/editor/xcode.rb +22 -0
  34. data/lib/watch_tower/errors.rb +25 -0
  35. data/lib/watch_tower/eye.rb +79 -0
  36. data/lib/watch_tower/project.rb +14 -0
  37. data/lib/watch_tower/project/.gitkeep +0 -0
  38. data/lib/watch_tower/project/any_based.rb +22 -0
  39. data/lib/watch_tower/project/git_based.rb +86 -0
  40. data/lib/watch_tower/project/init.rb +38 -0
  41. data/lib/watch_tower/project/path_based.rb +144 -0
  42. data/lib/watch_tower/server.rb +62 -0
  43. data/lib/watch_tower/server/.gitkeep +0 -0
  44. data/lib/watch_tower/server/app.rb +37 -0
  45. data/lib/watch_tower/server/assets/images/WatchTower.jpg +0 -0
  46. data/lib/watch_tower/server/assets/images/percentage.png +0 -0
  47. data/lib/watch_tower/server/assets/javascripts/application.js +3 -0
  48. data/lib/watch_tower/server/assets/javascripts/percentage.coffee +8 -0
  49. data/lib/watch_tower/server/assets/stylesheets/application.css +7 -0
  50. data/lib/watch_tower/server/assets/stylesheets/global.sass +71 -0
  51. data/lib/watch_tower/server/assets/stylesheets/project.sass +59 -0
  52. data/lib/watch_tower/server/configurations.rb +10 -0
  53. data/lib/watch_tower/server/configurations/asset.rb +43 -0
  54. data/lib/watch_tower/server/database.rb +105 -0
  55. data/lib/watch_tower/server/db/migrate/001_create_projects.rb +15 -0
  56. data/lib/watch_tower/server/db/migrate/002_create_files.rb +16 -0
  57. data/lib/watch_tower/server/db/migrate/003_create_time_entries.rb +12 -0
  58. data/lib/watch_tower/server/db/migrate/004_create_durations.rb +14 -0
  59. data/lib/watch_tower/server/db/migrate/005_add_hash_to_time_entries.rb +6 -0
  60. data/lib/watch_tower/server/db/migrate/006_add_hash_to_files.rb +6 -0
  61. data/lib/watch_tower/server/decorator.rb +21 -0
  62. data/lib/watch_tower/server/decorator/application_decorator.rb +91 -0
  63. data/lib/watch_tower/server/decorator/file_decorator.rb +38 -0
  64. data/lib/watch_tower/server/decorator/project_decorator.rb +51 -0
  65. data/lib/watch_tower/server/helpers.rb +13 -0
  66. data/lib/watch_tower/server/helpers/asset.rb +29 -0
  67. data/lib/watch_tower/server/helpers/improved_partials.rb +41 -0
  68. data/lib/watch_tower/server/models/duration.rb +11 -0
  69. data/lib/watch_tower/server/models/file.rb +31 -0
  70. data/lib/watch_tower/server/models/project.rb +17 -0
  71. data/lib/watch_tower/server/models/time_entry.rb +64 -0
  72. data/lib/watch_tower/server/public/assets/WatchTower-4d6de11e1bd34165ad91ac46fb711bf3.jpg +0 -0
  73. data/lib/watch_tower/server/public/assets/application-7829b53b5ece1a16d22dc3d00f329023.css +107 -0
  74. data/lib/watch_tower/server/public/assets/application-e0e6b7731aade460f680331e65cf0682.js +9359 -0
  75. data/lib/watch_tower/server/public/assets/percentage-d8589e21a5fc85d32a445f531ff8ab95.png +0 -0
  76. data/lib/watch_tower/server/vendor/assets/javascripts/jquery-ui.js +11729 -0
  77. data/lib/watch_tower/server/vendor/assets/javascripts/jquery.js +8981 -0
  78. data/lib/watch_tower/server/vendor/assets/javascripts/jquery_ujs.js +363 -0
  79. data/lib/watch_tower/server/views/.gitkeep +0 -0
  80. data/lib/watch_tower/server/views/_file.haml +9 -0
  81. data/lib/watch_tower/server/views/_project.haml +13 -0
  82. data/lib/watch_tower/server/views/index.haml +7 -0
  83. data/lib/watch_tower/server/views/layout.haml +32 -0
  84. data/lib/watch_tower/server/views/project.haml +12 -0
  85. data/lib/watch_tower/templates/config.yml +146 -0
  86. data/lib/watch_tower/templates/watchtower.plist +23 -0
  87. data/lib/watch_tower/version.rb +8 -0
  88. data/spec/factories.rb +45 -0
  89. data/spec/spec_helper.rb +26 -0
  90. data/spec/support/active_record.rb +44 -0
  91. data/spec/support/factory_girl.rb +6 -0
  92. data/spec/support/launchy.rb +3 -0
  93. data/spec/support/sinatra.rb +10 -0
  94. data/spec/support/timecop.rb +7 -0
  95. data/spec/watch_tower/appscript_spec.rb +6 -0
  96. data/spec/watch_tower/cli/install_spec.rb +16 -0
  97. data/spec/watch_tower/cli/open_spec.rb +14 -0
  98. data/spec/watch_tower/cli/start_spec.rb +17 -0
  99. data/spec/watch_tower/cli_spec.rb +15 -0
  100. data/spec/watch_tower/config_spec.rb +25 -0
  101. data/spec/watch_tower/editor/textmate_spec.rb +43 -0
  102. data/spec/watch_tower/editor/xcode_spec.rb +43 -0
  103. data/spec/watch_tower/editor_spec.rb +19 -0
  104. data/spec/watch_tower/eye_spec.rb +130 -0
  105. data/spec/watch_tower/project/git_based_spec.rb +131 -0
  106. data/spec/watch_tower/project/path_based_spec.rb +111 -0
  107. data/spec/watch_tower/project_spec.rb +82 -0
  108. data/spec/watch_tower/server/app_spec.rb +186 -0
  109. data/spec/watch_tower/server/decorator/project_decorator_spec.rb +60 -0
  110. data/spec/watch_tower/server/models/file_spec.rb +284 -0
  111. data/spec/watch_tower/server/models/project_spec.rb +165 -0
  112. data/spec/watch_tower/server/models/time_entry_spec.rb +37 -0
  113. data/spec/watch_tower/server_spec.rb +4 -0
  114. data/watch_tower.gemspec +80 -0
  115. metadata +450 -0
@@ -0,0 +1,21 @@
1
+ require 'draper'
2
+
3
+ module WatchTower
4
+ module Server
5
+ module Decorator
6
+ extend ::ActiveSupport::Autoload
7
+
8
+ autoload :ApplicationDecorator
9
+ autoload :ProjectDecorator
10
+ autoload :FileDecorator
11
+ end
12
+ end
13
+ end
14
+
15
+ # Monkey Patch Draper
16
+ Draper::Base.class_eval <<-END, __FILE__, __LINE__ + 1
17
+ def self.decorates(input)
18
+ self.model_class = "::WatchTower::Server::\#{input.to_s.camelize}".constantize
19
+ model_class.send :include, Draper::ModelSupport
20
+ end
21
+ END
@@ -0,0 +1,91 @@
1
+ module WatchTower
2
+ module Server
3
+ module Decorator
4
+ class ApplicationDecorator < Draper::Base
5
+
6
+ # Returns a human formatted time
7
+ #
8
+ # @return [String] The elapsed time formatted
9
+ def elapsed
10
+ if model.respond_to? :elapsed_time
11
+ humanize_time elapsed_time
12
+ else
13
+ ""
14
+ end
15
+ end
16
+
17
+ protected
18
+ def pluralize(num, word)
19
+ if num > 1
20
+ "#{num} #{word.pluralize}"
21
+ else
22
+ "#{num} #{word}"
23
+ end
24
+ end
25
+
26
+ # Humanize time
27
+ #
28
+ # @param [Integer] The number of seconds
29
+ # @return [String]
30
+ def humanize_time(time)
31
+ case
32
+ when time >= 1.day
33
+ humanize_day(time)
34
+ when time >= 1.hour
35
+ humanize_hour(time)
36
+ when time >= 1.minute
37
+ humanize_minute(time)
38
+ else
39
+ pluralize time, "second"
40
+ end
41
+ end
42
+
43
+ [:day, :hour, :minute].each do |t|
44
+ class_eval <<-END, __FILE__, __LINE__ + 1
45
+ protected
46
+ def humanize_#{t}(time)
47
+ seconds = 1.#{t}
48
+ num = (time / seconds).to_i
49
+ rest = time % seconds
50
+
51
+ time_str = pluralize num, "#{t}"
52
+
53
+ unless rest == 0
54
+ "\#{time_str}#{t == :minute ? ' and' : ','} \#{humanize_time(rest)}"
55
+ else
56
+ time_str
57
+ end
58
+ end
59
+ END
60
+ end
61
+
62
+ # Lazy Helpers
63
+ # PRO: Call Rails helpers without the h. proxy
64
+ # ex: number_to_currency(model.price)
65
+ # CON: Add a bazillion methods into your decorator's namespace
66
+ # and probably sacrifice performance/memory
67
+ #
68
+ # Enable them by uncommenting this line:
69
+ # lazy_helpers
70
+
71
+ # Shared Decorations
72
+ # Consider defining shared methods common to all your models.
73
+ #
74
+ # Example: standardize the formatting of timestamps
75
+ #
76
+ # def formatted_timestamp(time)
77
+ # h.content_tag :span, time.strftime("%a %m/%d/%y"),
78
+ # :class => 'timestamp'
79
+ # end
80
+ #
81
+ # def created_at
82
+ # formatted_timestamp(model.created_at)
83
+ # end
84
+ #
85
+ # def updated_at
86
+ # formatted_timestamp(model.updated_at)
87
+ # end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,38 @@
1
+ module WatchTower
2
+ module Server
3
+ module Decorator
4
+ class FileDecorator < ApplicationDecorator
5
+ decorates :file
6
+
7
+ # Accessing Helpers
8
+ # You can access any helper via a proxy
9
+ #
10
+ # Normal Usage: helpers.number_to_currency(2)
11
+ # Abbreviated : h.number_to_currency(2)
12
+ #
13
+ # Or, optionally enable "lazy helpers" by calling this method:
14
+ # lazy_helpers
15
+ # Then use the helpers with no proxy:
16
+ # number_to_currency(2)
17
+
18
+ # Defining an Interface
19
+ # Control access to the wrapped subject's methods using one of the following:
20
+ #
21
+ # To allow only the listed methods (whitelist):
22
+ # allows :method1, :method2
23
+ #
24
+ # To allow everything except the listed methods (blacklist):
25
+ # denies :method1, :method2
26
+
27
+ # Presentation Methods
28
+ # Define your own instance methods, even overriding accessors
29
+ # generated by ActiveRecord:
30
+ #
31
+ # def created_at
32
+ # h.content_tag :span, time.strftime("%a %m/%d/%y"),
33
+ # :class => 'timestamp'
34
+ # end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ module WatchTower
2
+ module Server
3
+ module Decorator
4
+ class ProjectDecorator < ApplicationDecorator
5
+ decorates :project
6
+
7
+ # Return an image representing the percentage of this project
8
+ #
9
+ # @return [String] image_tag
10
+ def percentage
11
+ max_elapsed = Project.order('elapsed_time DESC').first.elapsed_time
12
+
13
+ percentage = (elapsed_time * 100 / max_elapsed).to_i
14
+
15
+ <<-EHTML
16
+ <img src="#{asset_path('percentage.png')}" data-width="#{percentage}" />
17
+ EHTML
18
+ end
19
+
20
+ # Accessing Helpers
21
+ # You can access any helper via a proxy
22
+ #
23
+ # Normal Usage: helpers.number_to_currency(2)
24
+ # Abbreviated : h.number_to_currency(2)
25
+ #
26
+ # Or, optionally enable "lazy helpers" by calling this method:
27
+ # lazy_helpers
28
+ # Then use the helpers with no proxy:
29
+ # number_to_currency(2)
30
+
31
+ # Defining an Interface
32
+ # Control access to the wrapped subject's methods using one of the following:
33
+ #
34
+ # To allow only the listed methods (whitelist):
35
+ # allows :method1, :method2
36
+ #
37
+ # To allow everything except the listed methods (blacklist):
38
+ # denies :method1, :method2
39
+
40
+ # Presentation Methods
41
+ # Define your own instance methods, even overriding accessors
42
+ # generated by ActiveRecord:
43
+ #
44
+ # def created_at
45
+ # h.content_tag :span, time.strftime("%a %m/%d/%y"),
46
+ # :class => 'timestamp'
47
+ # end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,13 @@
1
+ require 'sinatra-snap'
2
+
3
+ module WatchTower
4
+ module Server
5
+ module Helpers
6
+ extend ::ActiveSupport::Autoload
7
+
8
+ # Sinatra helpers
9
+ autoload :ImprovedPartials
10
+ autoload :Asset
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module WatchTower
2
+ module Server
3
+ module Helpers
4
+ module Asset
5
+
6
+ def self.included(base)
7
+ base.send :include, InstanceMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+
12
+ # Define partial as a helper
13
+ helpers do
14
+ # Get the asset path of a given source
15
+ #
16
+ # Code taken from
17
+ # https://github.com/stevehodgkiss/sinatra-asset-pipeline/blob/master/app.rb#L11
18
+ #
19
+ # @param [String] The source file
20
+ # @return [String] The path to the asset
21
+ def asset_path(source)
22
+ "/assets/" + settings.sprockets.find_asset(source).digest_path
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,41 @@
1
+ module WatchTower
2
+ module Server
3
+ module Helpers
4
+ module ImprovedPartials
5
+
6
+ def self.included(base)
7
+ base.send :include, InstanceMethods
8
+ end
9
+
10
+ module InstanceMethods
11
+
12
+ # Define partial as a helper
13
+ helpers do
14
+ # Render a partial with support for collections
15
+ #
16
+ # stolen from http://github.com/cschneid/irclogger/blob/master/lib/partials.rb
17
+ # and made a lot more robust by Sam Elliott <sam@lenary.co.uk>
18
+ # https://gist.github.com/119874
19
+ #
20
+ # @param [Symbol] The template to render
21
+ # @param [Hash] Options
22
+ def partial(template, *args)
23
+ template_array = template.to_s.split('/')
24
+ template = template_array[0..-2].join('/') + "/_#{template_array[-1]}"
25
+ options = args.last.is_a?(Hash) ? args.pop : {}
26
+ options.merge!(:layout => false)
27
+ if collection = options.delete(:collection) then
28
+ collection.inject([]) do |buffer, member|
29
+ buffer << haml(:"#{template}", options.merge(:layout =>
30
+ false, :locals => {template_array[-1].to_sym => member}))
31
+ end.join("\n")
32
+ else
33
+ haml(:"#{template}", options)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,11 @@
1
+ module WatchTower
2
+ module Server
3
+ class Duration < ::ActiveRecord::Base
4
+ validates :file_id, presence: true
5
+ validates :date, presence: true
6
+ validates :duration, presence: true
7
+
8
+ belongs_to :file, counter_cache: true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module WatchTower
2
+ module Server
3
+ class File < ::ActiveRecord::Base
4
+ # Scopes
5
+ default_scope order('files.elapsed_time DESC')
6
+ scope :worked_on, -> { where('files.elapsed_time > ?', 0) }
7
+
8
+ # Validations
9
+ validates :project_id, presence: true
10
+ validates :path, presence: true
11
+ validates_uniqueness_of :path, sope: :project_id
12
+
13
+ # Associations
14
+ belongs_to :project, counter_cache: true
15
+ has_many :time_entries, dependent: :destroy
16
+ has_many :durations, dependent: :destroy
17
+
18
+ # Return the percent of this file
19
+ def percent
20
+ (elapsed_time * 100) / project.files.sum_elapsed_time
21
+ end
22
+
23
+ # Returns the sum of all elapsed time
24
+ #
25
+ # @return [Integer]
26
+ def self.sum_elapsed_time
27
+ sum(:elapsed_time)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module WatchTower
2
+ module Server
3
+ class Project < ::ActiveRecord::Base
4
+ # Scopes
5
+ default_scope order('projects.elapsed_time DESC')
6
+ scope :worked_on, -> { where('projects.elapsed_time > ?', 0) }
7
+
8
+ # Validations
9
+ validates :name, presence: true
10
+ validates :path, presence: true
11
+
12
+ # Associations
13
+ has_many :files, dependent: :destroy
14
+ has_many :time_entries, through: :files
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,64 @@
1
+ module WatchTower
2
+ module Server
3
+ class TimeEntry < ::ActiveRecord::Base
4
+
5
+ # Default pause time, in case nothing was given in the config file
6
+ DEFAULT_PAUSE_TIME = 30.minutes
7
+
8
+ # Validations
9
+ validates :file_id, presence: true
10
+ validates :mtime, presence: true
11
+ validates_uniqueness_of :mtime, scope: :file_id
12
+ validates :file_hash, presence: true
13
+
14
+
15
+ # Associations
16
+ belongs_to :file, counter_cache: true
17
+
18
+ # Callbacks
19
+ after_create :calculate_elapsed_time
20
+
21
+ protected
22
+ # Calculate the elapsed time between this time entry and the last one
23
+ # then update the durations table with the time difference, either by
24
+ # updating the duration for this day or by creating a new one for the
25
+ # next day
26
+ def calculate_elapsed_time
27
+ # Gather information about this and last time entry for this file
28
+ this_time_entry = self
29
+ last_time_entry = file.time_entries.where('id < ?', this_time_entry.id).order('id DESC').first
30
+ # Check the hash first
31
+ return if this_time_entry.file_hash == last_time_entry.try(:file_hash)
32
+ # Update the file's hash
33
+ file.file_hash = this_time_entry.file_hash
34
+ # Parse the date of the mtime
35
+ this_time_entry_date = self.mtime.to_date
36
+ last_time_entry_date = last_time_entry.mtime.to_date rescue nil
37
+ # Act upon the date
38
+ if this_time_entry_date == last_time_entry_date
39
+ # Calculate the time
40
+ time_entry_elapsed = this_time_entry.mtime - last_time_entry.mtime rescue 0
41
+ unless time_entry_elapsed > pause_time
42
+ # Update the file elapsed time
43
+ file.elapsed_time += time_entry_elapsed
44
+ # Update the project's elapsed time
45
+ file.project.elapsed_time += time_entry_elapsed
46
+ # Add this time to the durations table
47
+ d = file.durations.find_or_create_by_date(this_time_entry_date)
48
+ d.duration += time_entry_elapsed
49
+ d.save
50
+ end
51
+ end
52
+
53
+ # Save the file and project
54
+ file.save
55
+ file.project.save
56
+ end
57
+
58
+ # Get the
59
+ def pause_time
60
+ eval(Config[:pause_time]) rescue DEFAULT_PAUSE_TIME
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,107 @@
1
+ /*
2
+ * This is a manifest file that'll automatically include all the stylesheets available in this directory
3
+ * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
4
+ * the top of the compiled file, but it's generally better to create a new file per style scope.
5
+ */
6
+ h1 {
7
+ font-size: 2em; }
8
+
9
+ h2 {
10
+ font-size: 1em; }
11
+
12
+ #logo a {
13
+ color: black;
14
+ text-decoration: none;
15
+ display: block;
16
+ background: url("WatchTower-4d6de11e1bd34165ad91ac46fb711bf3.jpg") no-repeat;
17
+ width: 390px;
18
+ height: 94px; }
19
+ #logo a:visited, #logo a:hover {
20
+ text-decoration: none; }
21
+ #logo h1 {
22
+ float: left;
23
+ margin-top: 30px;
24
+ margin-left: 180px; }
25
+
26
+ .clearfix {
27
+ clear: both; }
28
+
29
+ a {
30
+ color: +#b36c6c;
31
+ text-decoration: none; }
32
+ a:hover, a:visited {
33
+ text-decoration: none; }
34
+
35
+ body {
36
+ background: #f7f7f7;
37
+ color: black;
38
+ font-family: Arial, Helvetica, sans-serif; }
39
+ body #wrapper {
40
+ width: 960px;
41
+ margin: 0 auto; }
42
+ body #wrapper #main {
43
+ border: 1px solid #cdcdcd;
44
+ -webkit-border-radius: 5px;
45
+ -moz-border-radius: 5px;
46
+ border-radius: 5px;
47
+ padding: 10px;
48
+ min-height: 200px;
49
+ background: white; }
50
+ body #wrapper #footer {
51
+ width: 960px;
52
+ text-align: center;
53
+ background: #d6afaf;
54
+ color: black;
55
+ padding: 5px 0;
56
+ font-size: 12px; }
57
+ body #wrapper #footer a {
58
+ color: black;
59
+ text-decoration: none; }
60
+ body #wrapper #footer a:visited, body #wrapper #footer a:hover {
61
+ text-decoration: none; }
62
+ #projects header .name {
63
+ float: left;
64
+ width: 200px;
65
+ font-size: 20px;
66
+ text-decoration: underline; }
67
+ #projects header .percentage {
68
+ width: 310px;
69
+ float: left;
70
+ font-size: 20px;
71
+ text-decoration: underline; }
72
+ #projects header .percentage .percentage_img {
73
+ height: 20px; }
74
+ #projects header .elapsed {
75
+ float: left;
76
+ font-size: 20px;
77
+ text-decoration: underline; }
78
+ #projects .project .name {
79
+ float: left;
80
+ width: 200px; }
81
+ #projects .project .percentage_img_container {
82
+ width: 310px;
83
+ float: left; }
84
+ #projects .project .percentage_img_container .percentage_img {
85
+ height: 20px; }
86
+ #projects .project .elapsed {
87
+ float: left; }
88
+
89
+ #project #files header .path {
90
+ font-size: 20px;
91
+ text-decoration: underline;
92
+ float: left;
93
+ width: 600px; }
94
+ #project #files header .elapsed {
95
+ font-size: 20px;
96
+ text-decoration: underline;
97
+ float: left; }
98
+ #project .path {
99
+ float: left;
100
+ width: 600px; }
101
+ #project .elapsed {
102
+ float: left; }
103
+ #project .percentage_img_container {
104
+ width: 310px;
105
+ float: left; }
106
+ #project .percentage_img_container .percentage_img {
107
+ height: 20px; }