watch_tower 0.0.0.1 → 0.0.1.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. data/.todo +3 -0
  2. data/.travis.yml +4 -0
  3. data/Gemfile +0 -4
  4. data/README.md +48 -2
  5. data/TODO +3 -0
  6. data/ci/travis.rb +4 -1
  7. data/lib/watch_tower/appscript.rb +65 -7
  8. data/lib/watch_tower/cli/install.rb +27 -3
  9. data/lib/watch_tower/cli/open.rb +2 -0
  10. data/lib/watch_tower/cli/start.rb +3 -1
  11. data/lib/watch_tower/cli.rb +2 -0
  12. data/lib/watch_tower/config.rb +2 -0
  13. data/lib/watch_tower/core_ext.rb +2 -0
  14. data/lib/watch_tower/editor/base_appscript.rb +28 -1
  15. data/lib/watch_tower/editor/base_ps.rb +25 -0
  16. data/lib/watch_tower/editor/textmate.rb +2 -0
  17. data/lib/watch_tower/editor/xcode.rb +2 -0
  18. data/lib/watch_tower/editor.rb +2 -0
  19. data/lib/watch_tower/errors.rb +2 -0
  20. data/lib/watch_tower/eye.rb +13 -2
  21. data/lib/watch_tower/file_tree.rb +78 -0
  22. data/lib/watch_tower/project/any_based.rb +2 -0
  23. data/lib/watch_tower/project/git_based.rb +2 -0
  24. data/lib/watch_tower/project/init.rb +2 -0
  25. data/lib/watch_tower/project/path_based.rb +2 -0
  26. data/lib/watch_tower/project.rb +2 -0
  27. data/lib/watch_tower/server/app.rb +37 -7
  28. data/lib/watch_tower/server/assets/javascripts/application.js +4 -0
  29. data/lib/watch_tower/server/assets/javascripts/datepicker.coffee +21 -0
  30. data/lib/watch_tower/server/assets/javascripts/file_tree.coffee +30 -0
  31. data/lib/watch_tower/server/assets/javascripts/percentage.coffee +15 -5
  32. data/lib/watch_tower/server/assets/stylesheets/application.css +1 -0
  33. data/lib/watch_tower/server/assets/stylesheets/date.sass +17 -0
  34. data/lib/watch_tower/server/assets/stylesheets/file_tree.sass +42 -0
  35. data/lib/watch_tower/server/assets/stylesheets/global.sass +7 -4
  36. data/lib/watch_tower/server/configurations/asset.rb +2 -0
  37. data/lib/watch_tower/server/configurations.rb +2 -0
  38. data/lib/watch_tower/server/database.rb +2 -0
  39. data/lib/watch_tower/server/db/migrate/001_create_projects.rb +2 -0
  40. data/lib/watch_tower/server/db/migrate/002_create_files.rb +2 -0
  41. data/lib/watch_tower/server/db/migrate/003_create_time_entries.rb +2 -0
  42. data/lib/watch_tower/server/db/migrate/004_create_durations.rb +2 -0
  43. data/lib/watch_tower/server/db/migrate/005_add_hash_to_time_entries.rb +2 -0
  44. data/lib/watch_tower/server/db/migrate/006_add_hash_to_files.rb +2 -0
  45. data/lib/watch_tower/server/db/migrate/007_add_editor_to_times_entries.rb +8 -0
  46. data/lib/watch_tower/server/db/migrate/008_rename_editor_to_editor_name_in_times_entries.rb +5 -0
  47. data/lib/watch_tower/server/db/migrate/009_remove_editor_index_from_time_entries.rb +7 -0
  48. data/lib/watch_tower/server/db/migrate/010_add_editor_version_to_times_entries.rb +7 -0
  49. data/lib/watch_tower/server/helpers/asset.rb +2 -0
  50. data/lib/watch_tower/server/helpers/improved_partials.rb +2 -0
  51. data/lib/watch_tower/server/helpers/presenters.rb +33 -0
  52. data/lib/watch_tower/server/helpers.rb +3 -0
  53. data/lib/watch_tower/server/models/duration.rb +9 -0
  54. data/lib/watch_tower/server/models/file.rb +17 -0
  55. data/lib/watch_tower/server/models/project.rb +30 -0
  56. data/lib/watch_tower/server/models/time_entry.rb +5 -0
  57. data/lib/watch_tower/server/presenters/application_presenter.rb +165 -0
  58. data/lib/watch_tower/server/presenters/file_presenter.rb +10 -0
  59. data/lib/watch_tower/server/presenters/project_presenter.rb +20 -0
  60. data/lib/watch_tower/server/presenters.rb +13 -0
  61. data/lib/watch_tower/server/public/assets/{WatchTower-4d6de11e1bd34165ad91ac46fb711bf3.jpg → WatchTower-58eff0713efffbc6054defddc879e0b1.jpg} +0 -0
  62. data/lib/watch_tower/server/public/assets/application-4e6971066e06aa53b0c8e52c764044d1.css +389 -0
  63. data/lib/watch_tower/server/public/assets/application-6a1be75d4fd6a545faceb638e47a2486.js +23778 -0
  64. data/lib/watch_tower/server/public/assets/calendar-379834cd6e6321a940b662ace47f3032.gif +0 -0
  65. data/lib/watch_tower/server/public/assets/calendar-blue-d6aa74feef7ee4287532761db99a6c0a.gif +0 -0
  66. data/lib/watch_tower/server/public/assets/calendar-green-3752fe2996091379c8d321f759039385.gif +0 -0
  67. data/lib/watch_tower/server/public/assets/jquery.datepick-9c8dfe3a4d40bcafc7b182e194c13836.css +227 -0
  68. data/lib/watch_tower/server/public/assets/{percentage-d8589e21a5fc85d32a445f531ff8ab95.png → percentage-d0176e99520c95e93eee63738ef5d487.png} +0 -0
  69. data/lib/watch_tower/server/vendor/assets/images/calendar-blue.gif +0 -0
  70. data/lib/watch_tower/server/vendor/assets/images/calendar-green.gif +0 -0
  71. data/lib/watch_tower/server/vendor/assets/images/calendar.gif +0 -0
  72. data/lib/watch_tower/server/vendor/assets/javascripts/jquery-datepick-ext.js +266 -0
  73. data/lib/watch_tower/server/vendor/assets/javascripts/jquery-datepick-validation.js +232 -0
  74. data/lib/watch_tower/server/vendor/assets/javascripts/jquery-datepick.js +2092 -0
  75. data/lib/watch_tower/server/vendor/assets/stylesheets/jquery.datepick.css +226 -0
  76. data/lib/watch_tower/server/views/_project.haml +10 -13
  77. data/lib/watch_tower/server/views/index.haml +9 -6
  78. data/lib/watch_tower/server/views/layout.haml +7 -4
  79. data/lib/watch_tower/server/views/project.haml +9 -11
  80. data/lib/watch_tower/server.rb +8 -1
  81. data/lib/watch_tower/templates/{watchtower.plist → watchtower.plist.erb} +5 -6
  82. data/lib/watch_tower/version.rb +12 -3
  83. data/lib/watch_tower.rb +20 -1
  84. data/spec/factories.rb +2 -0
  85. data/spec/watch_tower/appscript_spec.rb +36 -2
  86. data/spec/watch_tower/editor/base_appscript_spec.rb +83 -0
  87. data/spec/watch_tower/editor/textmate_spec.rb +37 -0
  88. data/spec/watch_tower/editor/xcode_spec.rb +6 -0
  89. data/spec/watch_tower/eye_spec.rb +21 -0
  90. data/spec/watch_tower/file_tree_spec.rb +156 -0
  91. data/spec/watch_tower/server/app_spec.rb +64 -20
  92. data/spec/watch_tower/server/models/file_spec.rb +93 -31
  93. data/spec/watch_tower/server/models/project_spec.rb +147 -18
  94. data/spec/watch_tower/server/models/time_entry_spec.rb +10 -0
  95. data/spec/watch_tower/server/{decorator/project_decorator_spec.rb → presenters/application_presenter_spec.rb} +24 -13
  96. data/spec/watch_tower/server/presenters/file_presenter_spec.rb +8 -0
  97. data/spec/watch_tower/server/presenters/project_presenter_spec.rb +130 -0
  98. data/watch_tower.gemspec +10 -4
  99. metadata +114 -74
  100. data/lib/watch_tower/server/decorator/application_decorator.rb +0 -91
  101. data/lib/watch_tower/server/decorator/file_decorator.rb +0 -38
  102. data/lib/watch_tower/server/decorator/project_decorator.rb +0 -51
  103. data/lib/watch_tower/server/decorator.rb +0 -21
  104. data/lib/watch_tower/server/public/assets/application-7829b53b5ece1a16d22dc3d00f329023.css +0 -107
  105. data/lib/watch_tower/server/public/assets/application-e0e6b7731aade460f680331e65cf0682.js +0 -9359
  106. data/lib/watch_tower/server/views/_file.haml +0 -9
@@ -0,0 +1,21 @@
1
+ window.date_filtering =
2
+ handleResponse: (response, textStatus, jqXHR) ->
3
+ ($ '.page').html(response)
4
+ percentage.applyPercentage()
5
+
6
+ updateMetaTags: (dates) ->
7
+ ($ 'meta[name="date_filtering_from"]').attr 'content', $.datepick.formatDate(dates[0])
8
+ ($ 'meta[name="date_filtering_to"]').attr 'content', $.datepick.formatDate(dates[1])
9
+
10
+ handleDatePicker: (dates) ->
11
+ options = { from_date: $.datepick.formatDate(dates[0]), to_date: $.datepick.formatDate(dates[1]) }
12
+ url = window.location.pathname
13
+ date_filtering.updateMetaTags dates
14
+ $.get url, options, date_filtering.handleResponse
15
+
16
+ jQuery ->
17
+ ($ '#date input').datepick
18
+ rangeSelect: true,
19
+ monthsToShow: 2,
20
+ alignment: 'bottomRight',
21
+ onClose: date_filtering.handleDatePicker
@@ -0,0 +1,30 @@
1
+ window.file_tree =
2
+ handle_button: (domId) ->
3
+ if ($ domId).hasClass "collapsed"
4
+ ($ domId).removeClass "collapsed"
5
+ ($ domId).addClass "expanded"
6
+ ($ domId).text '-'
7
+ file_tree.show_folder domId
8
+ else
9
+ ($ domId).removeClass "expanded"
10
+ ($ domId).addClass "collapsed"
11
+ ($ domId).text '+'
12
+ file_tree.hide_folder domId
13
+
14
+ show_folder: (button_domId) ->
15
+ ($ button_domId).parent().parent().children('.nested_folder').each (index, element) ->
16
+ ($ element).show()
17
+ ($ button_domId).parent().parent().children('.files').each (index, element) ->
18
+ ($ element).show()
19
+
20
+
21
+ hide_folder: (button_domId) ->
22
+ ($ button_domId).parent().parent().children('.nested_folder').each (index, element) ->
23
+ ($ element).hide()
24
+ ($ button_domId).parent().parent().children('.files').each (index, element) ->
25
+ ($ element).hide()
26
+
27
+ jQuery ($) ->
28
+ ($ '.collapsed').each (index, element) ->
29
+ ($ element).bind 'click', ->
30
+ file_tree.handle_button element
@@ -1,8 +1,18 @@
1
- animateGrowth = (domId, width) ->
2
- ($ domId).attr('width', width)
1
+ window.percentage =
2
+ applyPercentage: ->
3
+ ($ '.percentage_img').each (index, element) ->
4
+ width = ($ element).attr('data-width')
5
+ percentage.animateGrowth(element, width * 3)
6
+
7
+ animateGrowth: (domId, width, overGrow = false) ->
8
+ overGrowth = width + 50
9
+ overGrowth = 300 if overGrowth > 300
10
+ if overGrow
11
+ ($ domId).effect 'size', { to: { width: width } }, 1000
12
+ else
13
+ ($ domId).effect 'size', { to: { width: overGrowth } }, 1000, ->
14
+ percentage.animateGrowth domId, width, true
3
15
 
4
16
 
5
17
  jQuery ->
6
- ($ '.percentage_img').each (index, element) ->
7
- width = ($ element).attr('data-width')
8
- animateGrowth(element, width * 3)
18
+ percentage.applyPercentage()
@@ -3,5 +3,6 @@
3
3
  * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
4
4
  * the top of the compiled file, but it's generally better to create a new file per style scope.
5
5
  *= require_self
6
+ *= require jquery.datepick
6
7
  *= require_tree .
7
8
  */
@@ -0,0 +1,17 @@
1
+ aside#date
2
+ float: right
3
+ width: 200px
4
+ height: 30px
5
+ padding: 1px
6
+ border: 1px solid #CDCDCD
7
+ -webkit-border-radius: 5px
8
+ -moz-border-radius: 5px
9
+ border-radius: 5px
10
+
11
+ &:hover
12
+ border: 1px solid #AAAAAA
13
+
14
+ input
15
+ border: none
16
+ width: 201px
17
+ height: 21px
@@ -0,0 +1,42 @@
1
+ .file_tree
2
+ background: #f7f7f7
3
+ padding: 10px
4
+ min-height: 16px
5
+ border: 1px solid #cdcdcd
6
+ -webkit-border-radius: 5px
7
+ -moz-border-radius: 5px
8
+ border-radius: 5px
9
+
10
+ .collapsed, .expanded
11
+ float: left
12
+ text-align: center
13
+ width: 10px
14
+ height: 9px
15
+ margin-right: 10px
16
+ background: #fff
17
+ padding: 0 3px 7px 3px
18
+ -webkit-border-radius: 29px
19
+ -moz-border-radius: 29px
20
+ border-radius: 29px
21
+
22
+ &:hover
23
+ background: #5D4BFF
24
+ color: #fff
25
+ cursor: pointer
26
+
27
+ .name
28
+ float: left
29
+ .path
30
+ float: left
31
+ width: auto !important
32
+ .elapsed_time
33
+ float: right
34
+ .files
35
+ display: none
36
+ list-style: none
37
+ margin: 0
38
+
39
+ .nested_folder
40
+ display: none
41
+ &
42
+ margin-left: 15px
@@ -15,7 +15,7 @@ h2
15
15
  // Background stylesheet
16
16
  display: block
17
17
  // TODO: Very bad bad bad idea to hardcode assets, how can we reference an asset from sass ?
18
- background: url('WatchTower-4d6de11e1bd34165ad91ac46fb711bf3.jpg') no-repeat
18
+ background: url('WatchTower-58eff0713efffbc6054defddc879e0b1.jpg') no-repeat
19
19
  width: 390px
20
20
  height: 94px
21
21
 
@@ -48,13 +48,16 @@ body
48
48
 
49
49
  #main
50
50
  border: 1px solid #cdcdcd
51
- -webkit-border-radius: 5px
52
- -moz-border-radius: 5px
53
- border-radius: 5px
51
+ -webkit-border-radius: 5px 5px 0 0
52
+ -moz-border-radius: 5px 5px 0 0
53
+ border-radius: 5px 5px 0 0
54
54
  padding: 10px
55
55
  min-height: 200px
56
56
  background: #fff
57
57
 
58
+ .page
59
+ padding: 40px 0 0
60
+
58
61
  #footer
59
62
  width: 960px
60
63
  text-align: center
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  # Asset Pipeline
2
4
  require 'coffee-script'
3
5
  require 'uglifier'
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module WatchTower
2
4
  module Server
3
5
  module Configurations
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  require 'active_record'
2
4
 
3
5
  module WatchTower
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  class CreateProjects < ActiveRecord::Migration
2
4
  def change
3
5
  create_table :projects do |t|
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  class CreateFiles < ActiveRecord::Migration
2
4
  def change
3
5
  create_table :files do |t|
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  class CreateTimeEntries < ActiveRecord::Migration
2
4
  def change
3
5
  create_table :time_entries do |t|
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  class CreateDurations < ActiveRecord::Migration
2
4
  def change
3
5
  create_table :durations do |t|
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  class AddHashToTimeEntries < ActiveRecord::Migration
2
4
  def change
3
5
  add_column :time_entries, :file_hash, :string, null: false, default: ""
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  class AddHashToFiles < ActiveRecord::Migration
2
4
  def change
3
5
  add_column :files, :file_hash, :string, null: false, default: ""
@@ -0,0 +1,8 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class AddEditorToTimesEntries < ActiveRecord::Migration
4
+ def change
5
+ add_column :time_entries, :editor, :string, null: false, default: ""
6
+ add_index :time_entries, :editor
7
+ end
8
+ end
@@ -0,0 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class RenameEditorToEditorNameInTimesEntries < ActiveRecord::Migration
4
+ rename_column :time_entries, :editor, :editor_name
5
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class RemoveEditorIndexFromTimeEntries < ActiveRecord::Migration
4
+ def change
5
+ remove_index :time_entries, :editor
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ class AddEditorVersionToTimesEntries < ActiveRecord::Migration
4
+ def change
5
+ add_column :time_entries, :editor_version, :string, null: false, default: ""
6
+ end
7
+ end
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module WatchTower
2
4
  module Server
3
5
  module Helpers
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module WatchTower
2
4
  module Server
3
5
  module Helpers
@@ -0,0 +1,33 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module WatchTower
4
+ module Server
5
+ module Helpers
6
+ module Presenters
7
+
8
+ def self.included(base)
9
+ base.send :include, InstanceMethods
10
+ end
11
+
12
+ module InstanceMethods
13
+
14
+ # Define partial as a helper
15
+ helpers do
16
+ # Present an object
17
+ # Usually called with a block, the method yields the presenter into
18
+ # the block
19
+ #
20
+ # @param [ActiveRecord::Base] Object: The model to present
21
+ # @param [Nil | Object] klass: The klass to present
22
+ def present(object, klass = nil)
23
+ klass ||= "::WatchTower::Server::Presenters::#{object.class.to_s.split('::').last}Presenter".constantize
24
+ presenter = klass.new(object, self)
25
+ yield presenter if block_given?
26
+ presenter
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  require 'sinatra-snap'
2
4
 
3
5
  module WatchTower
@@ -8,6 +10,7 @@ module WatchTower
8
10
  # Sinatra helpers
9
11
  autoload :ImprovedPartials
10
12
  autoload :Asset
13
+ autoload :Presenters
11
14
  end
12
15
  end
13
16
  end
@@ -1,10 +1,19 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module WatchTower
2
4
  module Server
3
5
  class Duration < ::ActiveRecord::Base
6
+ # Scopes
7
+ scope :before_date, lambda { |date| where('date <= ?', Date.strptime(date, '%m/%d/%Y')) }
8
+ scope :after_date, lambda { |date| where('date >= ?', Date.strptime(date, '%m/%d/%Y')) }
9
+ scope :date_range, lambda { |from, to| after_date(from).before_date(to) }
10
+
11
+ # Validations
4
12
  validates :file_id, presence: true
5
13
  validates :date, presence: true
6
14
  validates :duration, presence: true
7
15
 
16
+ # Associations
8
17
  belongs_to :file, counter_cache: true
9
18
  end
10
19
  end
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module WatchTower
2
4
  module Server
3
5
  class File < ::ActiveRecord::Base
@@ -20,6 +22,21 @@ module WatchTower
20
22
  (elapsed_time * 100) / project.files.sum_elapsed_time
21
23
  end
22
24
 
25
+ # Return an ActiveRelation limited to a date range
26
+ #
27
+ # @param [String] From date
28
+ # @param [String] To date
29
+ # @return [ActiveRelation]
30
+ def self.date_range(from, to)
31
+ from = Date.strptime(from, '%m/%d/%Y')
32
+ to = Date.strptime(to, '%m/%d/%Y')
33
+
34
+ joins(:durations => :file).
35
+ where('durations.date >= ?', from).
36
+ where('durations.date <= ?', to).
37
+ select('DISTINCT files.*')
38
+ end
39
+
23
40
  # Returns the sum of all elapsed time
24
41
  #
25
42
  # @return [Integer]
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module WatchTower
2
4
  module Server
3
5
  class Project < ::ActiveRecord::Base
@@ -12,6 +14,34 @@ module WatchTower
12
14
  # Associations
13
15
  has_many :files, dependent: :destroy
14
16
  has_many :time_entries, through: :files
17
+ has_many :durations, through: :files
18
+
19
+ # Return the percent of this file
20
+ def percent
21
+ (elapsed_time * 100) / self.class.sum_elapsed_time
22
+ end
23
+
24
+ # Return an ActiveRelation limited to a date range
25
+ #
26
+ # @param [String] From date
27
+ # @param [String] To date
28
+ # @return [ActiveRelation]
29
+ def self.date_range(from, to)
30
+ from = Date.strptime(from, '%m/%d/%Y')
31
+ to = Date.strptime(to, '%m/%d/%Y')
32
+
33
+ joins(:durations => :file).
34
+ where('durations.date >= ?', from).
35
+ where('durations.date <= ?', to).
36
+ select('DISTINCT projects.*')
37
+ end
38
+
39
+ # Returns the sum of all elapsed time
40
+ #
41
+ # @return [Integer]
42
+ def self.sum_elapsed_time
43
+ sum(:elapsed_time)
44
+ end
15
45
  end
16
46
  end
17
47
  end
@@ -1,3 +1,5 @@
1
+ # -*- encoding: utf-8 -*-
2
+
1
3
  module WatchTower
2
4
  module Server
3
5
  class TimeEntry < ::ActiveRecord::Base
@@ -10,10 +12,13 @@ module WatchTower
10
12
  validates :mtime, presence: true
11
13
  validates_uniqueness_of :mtime, scope: :file_id
12
14
  validates :file_hash, presence: true
15
+ validates :editor_name, presence: true
16
+ validates :editor_version, presence: true
13
17
 
14
18
 
15
19
  # Associations
16
20
  belongs_to :file, counter_cache: true
21
+ has_many :durations
17
22
 
18
23
  # Callbacks
19
24
  after_create :calculate_elapsed_time
@@ -0,0 +1,165 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module WatchTower
4
+ module Server
5
+ module Presenters
6
+ class ApplicationPresenter
7
+
8
+ attr_reader :model, :template
9
+
10
+ # Presents a model
11
+ #
12
+ # @param [Symbol] name
13
+ def self.presents(name)
14
+ define_method name do
15
+ @model
16
+ end
17
+ end
18
+
19
+ # Initialise the presenter
20
+ #
21
+ # @param [ActiveRecord::Base] Model
22
+ # @param [Object] Template
23
+ def initialize(model, template)
24
+ @model = model
25
+ @template = template
26
+ end
27
+
28
+ # Overwrite Kernel#method_missing to invoke either the model or the
29
+ # template's method if present, if not Kernel#method_missing will be
30
+ # called
31
+ #
32
+ # @param [Symbol] method: The method name
33
+ # @param [Array] arguments
34
+ # @param [Block]
35
+ def method_missing(method, *args, &block)
36
+ if model.respond_to?(method)
37
+ model.send(method, *args, &block)
38
+ elsif template.respond_to?(method)
39
+ template.send(method, *args, &block)
40
+ else
41
+ super
42
+ end
43
+ end
44
+
45
+ # Returns a human formatted time
46
+ #
47
+ # @param [Integer] elapsed_time
48
+ # @return [String] The elapsed time formatted
49
+ def elapsed(elapsed_time = nil)
50
+ return "" if elapsed_time.nil? && !model.respond_to?(:elapsed_time)
51
+ elapsed_time ||= model.elapsed_time
52
+ humanize_time elapsed_time
53
+ end
54
+
55
+ protected
56
+ def pluralize(num, word)
57
+ if num > 1
58
+ "#{num} #{word.pluralize}"
59
+ else
60
+ "#{num} #{word}"
61
+ end
62
+ end
63
+
64
+ # Humanize time
65
+ #
66
+ # @param [Integer] The number of seconds
67
+ # @return [String]
68
+ def humanize_time(time)
69
+ case
70
+ when time >= 1.day
71
+ humanize_day(time)
72
+ when time >= 1.hour
73
+ humanize_hour(time)
74
+ when time >= 1.minute
75
+ humanize_minute(time)
76
+ else
77
+ pluralize time, "second"
78
+ end
79
+ end
80
+
81
+ [:day, :hour, :minute].each do |t|
82
+ class_eval <<-END, __FILE__, __LINE__ + 1
83
+ protected
84
+ def humanize_#{t}(time)
85
+ seconds = 1.#{t}
86
+ num = (time / seconds).to_i
87
+ rest = time % seconds
88
+
89
+ time_str = pluralize num, "#{t}"
90
+
91
+ unless rest == 0
92
+ "\#{time_str}#{t == :minute ? ' and' : ','} \#{humanize_time(rest)}"
93
+ else
94
+ time_str
95
+ end
96
+ end
97
+ END
98
+ end
99
+
100
+ # Parse a file tree
101
+ #
102
+ # @param [FileTree] tree
103
+ # @param [Boolean] root
104
+ # @return [String] HTML of the file tree
105
+ def parse_file_tree(tree, root = false)
106
+ # Create the root element
107
+ if root
108
+ html = '<article class="file_tree">'
109
+ html << '<div id="root" class="folder">'
110
+ else
111
+ folder_name = ::File.basename(tree.base_path)
112
+ html = %(<div id="nested_#{folder_name}" class="nested_folder">)
113
+ end
114
+ # Open the wrapper
115
+ html << '<div>'
116
+ # Add the collapsed span
117
+ html << '<span class="collapsed">+</span>'
118
+ # Add the name
119
+ if root
120
+ html << '<span class="name">Project</span>'
121
+ else
122
+ html << %(<span class="name">#{folder_name}</span>)
123
+ end
124
+ # Add the elapsed time
125
+ html << %(<span class="elapsed_time">#{elapsed(tree.elapsed_time)}</span>)
126
+ # End with a clearfix element
127
+ html << '<div class="clearfix"></div>'
128
+ # Close the wrapper
129
+ html << '</div>'
130
+ # Add the nested_tree if available
131
+ if tree.nested_tree.any?
132
+ tree.nested_tree.each_pair do |folder, nested_tree|
133
+ html << parse_file_tree(nested_tree)
134
+ end
135
+ end
136
+ # Add the files
137
+ if tree.files.any?
138
+ # Open the files 's ul
139
+ html << '<ul class="files">'
140
+ tree.files.each do |file|
141
+ # Open the file's li
142
+ html << '<li class="file">'
143
+ # Add the path
144
+ html << %(<span class="path">#{file[:path]}</span>)
145
+ # Add the elapsed time
146
+ html << %(<span class="elapsed_time">#{elapsed(file[:elapsed_time])}</span>)
147
+ # End with a clearfix element
148
+ html << '<div class="clearfix"></div>'
149
+ # Close the file's li
150
+ html << '</li>'
151
+ end
152
+ # Close the files 's ul
153
+ html << '</ul>'
154
+ end
155
+ # Close the root div
156
+ html << "</div>"
157
+ # Clode the article if it is the root element
158
+ html << "</article>" if root
159
+ # Finally return the whole thing
160
+ html
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,10 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module WatchTower
4
+ module Server
5
+ module Presenters
6
+ class FilePresenter < ApplicationPresenter
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module WatchTower
4
+ module Server
5
+ module Presenters
6
+ class ProjectPresenter < ApplicationPresenter
7
+
8
+ # Return a file tree representation of a bunch of files
9
+ #
10
+ # @return [String] HTML representation of a file tree
11
+ def file_tree(files)
12
+ # Create a FileTree
13
+ file_tree = FileTree.new(files.first.project.path, files)
14
+ # Parse and return the tree
15
+ parse_file_tree(file_tree, true)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module WatchTower
4
+ module Server
5
+ module Presenters
6
+ extend ::ActiveSupport::Autoload
7
+
8
+ autoload :ApplicationPresenter
9
+ autoload :ProjectPresenter
10
+ autoload :FilePresenter
11
+ end
12
+ end
13
+ end