logtime 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.gitignore +5 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +40 -0
  6. data/LICENSE.md +21 -0
  7. data/VERSION +1 -0
  8. data/bin/logtime +3 -0
  9. data/certs/paperback.pem +21 -0
  10. data/config/application.rb +6 -0
  11. data/config/database.rb +4 -0
  12. data/config/initialisation.rb +8 -0
  13. data/config/schema.rb +41 -0
  14. data/lib/app/commands/add.rb +41 -0
  15. data/lib/app/commands/list.rb +56 -0
  16. data/lib/app/commands/start.rb +28 -0
  17. data/lib/app/commands/stop.rb +29 -0
  18. data/lib/app/helpers/estimate.rb +13 -0
  19. data/lib/app/helpers/statistics.rb +45 -0
  20. data/lib/app/helpers/time_to_words.rb +33 -0
  21. data/lib/app/logtime.rb +67 -0
  22. data/lib/app/models/log.rb +42 -0
  23. data/lib/app/models/series.rb +35 -0
  24. data/lib/app/models/tag.rb +9 -0
  25. data/lib/app/subcommands/keyword.rb +214 -0
  26. data/lib/app/subcommands/series.rb +58 -0
  27. data/lib/app/subcommands/tags.rb +79 -0
  28. data/lib/commands/add.rb +41 -0
  29. data/lib/commands/list.rb +67 -0
  30. data/lib/commands/start.rb +28 -0
  31. data/lib/commands/stop.rb +29 -0
  32. data/lib/commands/version.rb +12 -0
  33. data/lib/helpers/estimate.rb +13 -0
  34. data/lib/helpers/statistics.rb +48 -0
  35. data/lib/helpers/time_difference.rb +20 -0
  36. data/lib/helpers/time_display.rb +5 -0
  37. data/lib/helpers/time_to_words.rb +33 -0
  38. data/lib/logtime.rb +68 -0
  39. data/lib/models/log.rb +47 -0
  40. data/lib/models/series.rb +35 -0
  41. data/lib/models/tag.rb +9 -0
  42. data/lib/subcommands/keyword.rb +214 -0
  43. data/lib/subcommands/series.rb +59 -0
  44. data/lib/subcommands/tags.rb +37 -0
  45. data/logtime.gemspec +23 -0
  46. data.tar.gz.sig +2 -0
  47. metadata +167 -0
  48. metadata.gz.sig +0 -0
@@ -0,0 +1,214 @@
1
+ class Keywords < Thor
2
+ # desc "add <account> <keyword>", "Add a new <keyword> to an <account>"
3
+ # option :interactive, :type => :boolean, :alias => :i
4
+ # def add(account, keyword=nil)
5
+ # account = Account.find_by(:name => account)
6
+
7
+ # if account.blank?
8
+ # say 'no account found', :red
9
+ # else
10
+ # if options[:interactive]
11
+ # say "To stop adding keywords type exit", :cyan
12
+ # while true
13
+ # keyword = ask ':'
14
+ # break if keyword == 'exit'
15
+ # if Keyword.new(:keyword => keyword, :account_id => account.id).save
16
+ # say 'added', :green
17
+ # else
18
+ # say 'failed', :red
19
+ # end
20
+ # end
21
+ # else
22
+ # keyword.split(',').each do |k|
23
+ # k = Keyword.new(:keyword => k, :account_id => account.id)
24
+
25
+ # if k.save
26
+ # say k.keyword + ' keyword added', :green
27
+ # else
28
+ # say 'could not add keyword!', :red
29
+ # error k.errors.first[0].to_s + ' ' + k.errors.first[1]
30
+ # end
31
+ # end
32
+ # end
33
+ # end
34
+ # end
35
+
36
+ # desc "delete <account> <keyword>", "Removes <keyword> on an <account>"
37
+ # def delete(account, keyword)
38
+ # account = Account.search(account)
39
+
40
+ # if account.blank?
41
+ # say 'no account found', :red
42
+ # else
43
+ # keyword.split(',').each do |k|
44
+ # k = Keyword.search(k)
45
+
46
+ # if k.blank?
47
+ # say 'could not find keyword!', :red
48
+ # else
49
+ # say k.keyword + ' removed', :green if k.destroy
50
+ # end
51
+ # end
52
+ # end
53
+ # end
54
+
55
+ # desc "list", "Lists keywords belonging to an <account>"
56
+ # option :account
57
+ # def list(account=nil)
58
+ # if account.nil?
59
+ # keywords = Keyword.all
60
+
61
+ # table = [['# Keyword', '# Position', '']] # header
62
+ # keywords.each do |k|
63
+ # stat = k.keyword_statistics.last unless k.keyword_statistics.empty?
64
+ # table << [k.keyword, stat.position || '', stat.created_at.pretty ] # row
65
+ # end
66
+
67
+ # say ''
68
+ # print_table table
69
+ # say ''
70
+ # else
71
+ # account = Account.search(account)
72
+ # if account.blank?
73
+ # say 'no account found', :red
74
+ # elsif account.keywords.empty?
75
+ # say 'no keywords exist for that account yet', :red
76
+ # else
77
+ # table = [['# Keyword', '# Position', '']] # header
78
+ # account.keywords.each do |k|
79
+ # stat = k.keyword_statistics.last unless k.keyword_statistics.empty?
80
+ # table << [k.keyword, stat.position || '', stat.created_at.pretty] # row
81
+ # end
82
+
83
+ # say ''
84
+ # print_table table
85
+ # say ''
86
+ # end
87
+ # end
88
+ # end
89
+
90
+ # desc "stats", "Compare keywords over time"
91
+ # option :search, :default => 'year', :banner => 'year,month,week,day'
92
+ # option :length, :type => :numeric, :default => 1
93
+ # option :engine, :default => :google
94
+ # def stats(account)
95
+ # account = Account.search(account)
96
+
97
+ # if account.blank?
98
+ # say 'no account found', :red
99
+ # elsif account.keywords.empty?
100
+ # say 'no keywords exist for that account yet', :red
101
+ # else
102
+
103
+ # table = [['# Keyword', 'n', 'High', 'Low', 'Current', 'Delta']] # header
104
+ # searched = 0;
105
+ # account.keywords.each do |k|
106
+ # stats = KeywordStatistic.send options[:search], k.id, options[:length], options[:engine]
107
+ # stats.order(:position)
108
+
109
+ # if !stats.empty?
110
+ # delta = stats.last.position - stats.first.position
111
+ # delta = '' if delta == 0
112
+ # table << [k.keyword, stats.size, stats.last.position, stats.first.position, stats.last.position, delta] # row
113
+ # searched = searched + stats.size
114
+ # end
115
+ # end
116
+
117
+ # if searched == 0
118
+ # say 'these keywords have not been indexed yet', :red
119
+ # else
120
+ # say ''
121
+ # say account.name + ' has ' + searched.to_s + ' recorded positions for ' + account.keywords.size.to_s + ' keywords', :green
122
+ # say ''
123
+ # print_table table
124
+ # say ''
125
+ # end
126
+ # end
127
+ # end
128
+
129
+ # #desc "import", "imports csv file"
130
+ # #options :file
131
+ # #def import(file)
132
+
133
+ # #end
134
+
135
+ # desc "index", "gets keyword position and saves it for later use"
136
+ # def index(account)
137
+ # account = Account.search(account)
138
+ # if account.blank?
139
+ # say 'no account found', :red
140
+ # else
141
+ # table = [['# Keyword', '# Google', '# GoogleUS']] # header
142
+ # account.keywords.each do |k|
143
+ # # Google
144
+ # google = ::Ranking.new(:keyword => k.keyword, :url => account.domain, :limit =>100).from_googleAU
145
+ # if !google.blank?
146
+ # KeywordStatistic.new(:keyword_id => k.id, :engine => :google, :position => google).save
147
+ # say '.', :green, false
148
+ # else
149
+ # say '.', :red, false
150
+ # end
151
+
152
+ # # GoogleUS
153
+ # googleUS = ::Ranking.new(:keyword => k.keyword, :url => account.domain, :limit =>100).from_googleUS
154
+ # if !googleUS.blank?
155
+ # KeywordStatistic.new(:keyword_id => k.id, :engine => :googleUS, :position => googleUS).save
156
+ # say '.', :green, false
157
+ # else
158
+ # say '.', :red, false
159
+ # end
160
+
161
+ # # Bing
162
+ # # bing = ::Ranking.new(:keyword => k.keyword, :url => account.domain, :limit =>100).from_bingAU
163
+ # # if !bing.blank?
164
+ # # KeywordStatistic.new(:keyword_id => k.id, :engine => :bing, :position => bing).save
165
+ # # say '.', :green, false
166
+ # # else
167
+ # # say '.', :red, false
168
+ # # end
169
+
170
+
171
+ # # Yahoo
172
+ # # yahoo = ::Ranking.new(:keyword => k.keyword, :url => account.domain, :limit =>100).from_yahooAU
173
+ # # if !yahoo.blank?
174
+ # # KeywordStatistic.new(:keyword_id => k.id, :engine => :yahoo, :position => yahoo).save
175
+ # # say '.', :green, false
176
+ # # else
177
+ # # say '.', :red, false
178
+ # # end
179
+
180
+ # table << [k.keyword, google, googleUS]
181
+ # end
182
+
183
+ # say ''
184
+ # print_table table
185
+ # say ''
186
+ # end
187
+ # end
188
+
189
+ # desc "export", "export keywords with positions"
190
+ # option :file
191
+ # def export(account)
192
+ # account = Account.search(account)
193
+ # if account.blank?
194
+ # say 'no account found', :red
195
+ # else
196
+ # table = [['# Date', '# Keyword', '# Position']] # header
197
+
198
+ # options[:file] = 'export.csv' unless options.includes? :file
199
+ # CSV.open(options[:file], 'w') do |csv|
200
+ # account.keywords.each do |keyword|
201
+ # keyword.keyword_statistics.each do |stat|
202
+ # row = [stat.created_at, keyword.keyword, stat.position]
203
+ # table << row
204
+ # csv << row
205
+ # end
206
+ # end
207
+ # end
208
+
209
+ # say ''
210
+ # print_table table
211
+ # say ''
212
+ # end
213
+ # end
214
+ end
@@ -0,0 +1,58 @@
1
+ module SubCommands
2
+ class Series < Thor
3
+ desc "ls", "list series"
4
+ option :order, :type => :string, :default => "updated_at"
5
+ def ls
6
+ series = ::Series.all.order(options[:order].to_sym => :desc)
7
+
8
+ series_stats = Statistics.new
9
+ table = [['#', 'log', 'start', 'end', 'hours']] # header
10
+ series.each do |s|
11
+ next if s.log_id.blank?
12
+
13
+ start = s.start
14
+ finish = s.end
15
+
16
+ # total time
17
+ series_stats << timer = start.difference(finish || Time.now)
18
+
19
+ start = start.display if start
20
+ finish = finish.display if finish
21
+
22
+ table << [s.id,
23
+ Log.find(s.log_id).name,
24
+ start, finish || "active",
25
+ (timer/3600).round(2).to_s] # row
26
+ end
27
+ puts ''
28
+ print_table table
29
+ puts ''
30
+
31
+ say [series.count.to_s, "series out of", ::Series.count].join(' '), :cyan
32
+ say [(series_stats.mean/3600).round(2), " hours avg"].join(' '), :cyan
33
+ end
34
+
35
+ desc "rm [ID]", "remove series"
36
+ option :confirm, :type => :boolean, :default => false, :alias => '-y'
37
+ def rm(id)
38
+ if ::Series.where(id: id).count == 0
39
+ say "Series #" + id.to_s + " not found", :red
40
+ exit
41
+ end
42
+
43
+ series = ::Series.find(id)
44
+
45
+ if !options[:confirm]
46
+ say "You must --confirm before removing this series", :red
47
+ exit
48
+ end
49
+
50
+ if !series.finished?
51
+ series.log.deactivate
52
+ end
53
+
54
+ destroyed = series.destroy
55
+ say "Series #" + id.to_s + " has been destroyed", :green
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,79 @@
1
+ module SubCommands
2
+ class Tags < Thor
3
+ desc "add", "create new tag"
4
+ def add(tag)
5
+ tag = ::Tag.new(tag: tag.strip)
6
+
7
+ if !tag.save
8
+ tag.errors.full_messages.each do |error|
9
+ say error, :red
10
+ end
11
+ exit 1
12
+ end
13
+
14
+ say tag.tag + " added!", :green
15
+ end
16
+
17
+ desc "rm", "remove tag"
18
+ # option :prune, :type => :boolean, :alias => '-p', :default => false not implemented yet
19
+ def rm(tag)
20
+ tag = ::Tag.where(tag: tag)
21
+ p tag.inspect
22
+ end
23
+
24
+ desc "ls", "list tags"
25
+ option :order, :type => :string, :default => "created_at"
26
+ def ls
27
+ tags = ::Tag.order(options[:order].to_sym => :desc)
28
+
29
+ table = [['#', '']] # header
30
+ tags.each do |tag|
31
+ table << [tag.id, tag.tag] # row
32
+ end
33
+ print_table table
34
+ end
35
+
36
+ # desc "add <name> <domain>", "Adds an account that owns a <domain>"
37
+ # def add(name, domain)
38
+ # account = Account.new({
39
+ # :name => name,
40
+ # :domain => domain
41
+ # })
42
+
43
+ # if account.save
44
+ # say name + ' added', :green
45
+ # else
46
+ # say 'could not add account!', :red
47
+ # error account.errors.first[0].to_s + ' ' + account.errors.first[1]
48
+ # end
49
+ # end
50
+
51
+ # desc "delete <account>", "Deletes <account>"
52
+ # #option :force, :type => :boolean, :alias => '-f', :default => false
53
+ # def delete(account)
54
+ # account = Account.search(account)
55
+
56
+ # if account.blank?
57
+ # say 'no account found', :red
58
+ # else
59
+ # say 'WARNING ', :red, false
60
+ # should_destroy = ask "Are you sure you want to delete " + account.name + "?", :limited_to => ["y", "n"]
61
+ # account.destroy and say account.name + ' destroyed!', :red if should_destroy == 'y'
62
+ # say 'no action was taken', :green if should_destroy == 'n'
63
+ # end
64
+ # end
65
+
66
+ # desc "list", "Lists all accounts"
67
+ # def list
68
+ # accounts = Account.all
69
+
70
+ # table = [['# Name', '# Domain', '# Keywords']] # header
71
+ # accounts.each do |a|
72
+ # table << [a.name, a.domain, a.keywords.size] # row
73
+ # end
74
+ # say ''
75
+ # print_table table
76
+ # say ''
77
+ # end
78
+ end
79
+ end
@@ -0,0 +1,41 @@
1
+ module Commands
2
+ module Add
3
+ def self.included(thor)
4
+ thor.class_eval do
5
+ option :tag, :type => :string, :alias => '-t', default: ""
6
+ option :estimate, :type => :string, :alias => '-e', default: false
7
+ option :start, :type => :boolean, default: true
8
+ desc "add [NAME]", "create new log"
9
+ def add(name)
10
+ log = Log.new(name: name).tag(options[:tag])
11
+
12
+ # estimate option
13
+ if options[:estimate]
14
+ estimation = ChronicDuration.parse(options[:estimate]) if !estimation
15
+ if !estimation
16
+ say "could not parse estimation time: " + options[:estimate], :red
17
+ exit
18
+ end
19
+ log.estimation = estimation
20
+ end
21
+
22
+ # save log
23
+ if !log.save
24
+ log.errors.full_messages.each do |error|
25
+ say error, :red
26
+ end
27
+ exit
28
+ end
29
+
30
+ # start option
31
+ if options[:start]
32
+ log.activate if Series.begin(log_id: log.id)
33
+ say name.to_s + " added and started", :green
34
+ else
35
+ say name.to_s + " added", :green
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ module Commands
2
+ module List
3
+ def self.included(thor)
4
+ thor.class_eval do
5
+ option :active, :type => :boolean, :default => true
6
+ #option :tag, :type => :string, :alias => '-t'
7
+ desc "ls", "list time series"
8
+ def ls(tag='')
9
+ logs = Log.where(active: options[:active]).includes(:series).includes(:tags)
10
+ logs = logs.tagged(tag) if !tag.blank?
11
+
12
+ creep_stats = Statistics.new
13
+ table = [['#', 'name', 'active', 'tags', 'series', 'time', 'estimate', 'creep %']] # header
14
+ logs.each do |log|
15
+
16
+ # tags
17
+ tags = log.tags.map do |t|
18
+ t.tag
19
+ end
20
+
21
+ # only show logs with tag, if selected
22
+ # if !tag.blank?
23
+ # next unless tags.include? tag
24
+ # end
25
+
26
+ # active text
27
+ active = "ACTIVE" if log.active == "t"
28
+
29
+ # total counter
30
+ total_time = (log.total/3600).round(2)
31
+
32
+ # estimation creep
33
+ if log.estimation
34
+ creep = (total_time/log.estimation)*100
35
+ creep_stats << creep
36
+ creep = creep.round(3)
37
+ estimation = (log.estimation/3600).round(3) # log estimation to hours
38
+ end
39
+
40
+ table << [log.id,
41
+ log.name,
42
+ active || '',
43
+ tags,
44
+ log.series.count || '',
45
+ total_time || '',
46
+ estimation || '',
47
+ creep || '']
48
+ end
49
+
50
+ if logs.count == 0
51
+ puts ""
52
+ say "No logs found", :red
53
+ puts ""
54
+ exit
55
+ else
56
+ puts ""
57
+ print_table table
58
+ puts ""
59
+ say [logs.count.to_s, "logs out of", Log.count.to_s].join(' '), :cyan
60
+ say [creep_stats.count, "estimations"].join(' '), :cyan
61
+ say [creep_stats.mean.round(2), "percent mean creep"].join(' '), :cyan
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,28 @@
1
+ module Commands
2
+ module Start
3
+ def self.included(thor)
4
+ thor.class_eval do
5
+ desc "start [NAME]", "start new timer"
6
+ def start(name)
7
+
8
+ # not found
9
+ if Log.where(name: name).count == 0
10
+ say name.to_s + " not found", :red
11
+ exit
12
+ end
13
+
14
+ log = Log.find_by(name: name)
15
+
16
+ # already active?
17
+ if log.active == "t"
18
+ say name.to_s + " already active", :red
19
+ exit
20
+ end
21
+
22
+ # begin new series, activate log
23
+ series = Series.begin(log_id: log.id) and log.activate
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ module Commands
2
+ module Stop
3
+ def self.included(thor)
4
+ thor.class_eval do
5
+ desc "stop [NAME]", "stop active timer"
6
+ def stop(name)
7
+ if Log.where(name: name).count == 0
8
+ say name.to_s + " not found", :red
9
+ exit
10
+ end
11
+
12
+ log = Log.find_by(name: name)
13
+ if log.active == "f"
14
+ say name.to_s + " not active", :red
15
+ exit
16
+ end
17
+
18
+ series = log.series.last
19
+ if series.stop and log.deactivate
20
+ say name + " stopped!", :green
21
+
22
+ # this_series = log.series.last
23
+ say (series.total/3600).round(3).to_s + " hours out of " + (log.total/3600).round(3).to_s, :cyan
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,12 @@
1
+ module Commands
2
+ module Version
3
+ def self.included(thor)
4
+ thor.class_eval do
5
+ desc "version", ""
6
+ def version
7
+ io.read 'VERSION'
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module EstimateHelper
2
+ class Estimate
3
+ def initialize(time=0)
4
+ @time = time
5
+ end
6
+
7
+ def difference(estimate=false)
8
+ if !estimate
9
+ estimate = Time.now
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,48 @@
1
+ module StatisticsHelper
2
+ class Series < Array
3
+ def sum
4
+ inject(0.0) { |result, el| result + el }
5
+ end
6
+
7
+ def mean
8
+ return 0 if size == 0
9
+ avg = sum / size
10
+ end
11
+
12
+ alias :average :mean
13
+ alias :avg :mean
14
+ end
15
+
16
+ class Statistics
17
+ def initialize
18
+ @statistics = Series.new
19
+ end
20
+
21
+ def add(statistic)
22
+ @statistics << statistic
23
+ end
24
+
25
+ def count
26
+ @statistics.count
27
+ end
28
+
29
+ def mean
30
+ @statistics.mean
31
+ end
32
+
33
+ def <<(statistic)
34
+ add(statistic)
35
+ end
36
+
37
+ def +(statistics)
38
+ @statistics + statistics.data
39
+ end
40
+
41
+ def data
42
+ @statistics
43
+ end
44
+
45
+ alias :average :mean
46
+ alias :avg :mean
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ module TimeDifferenceHelper
2
+ def difference(relative)
3
+ if relative > self
4
+ difference = relative - self
5
+ else
6
+ difference = self - relative
7
+ end
8
+
9
+ # hours = difference / 3600
10
+ # difference -= hours * 3600
11
+ # minutes = difference / 60
12
+ # difference -= minutes * 60
13
+ # seconds = difference
14
+ # "#{hours.to_s.rjust(2, '0')}:#{minutes.to_s.rjust(2, '0')}:#{seconds.to_s.rjust(2, '0')}"
15
+
16
+ # hours = difference / 3600
17
+ return difference
18
+ end
19
+ Time.send :include, self
20
+ end
@@ -0,0 +1,5 @@
1
+ module TimeDisplayHelper
2
+ def display_time(time)
3
+ time.strftime("%H:%M:%S %d/%m/%y") if time.is_a? Time
4
+ end
5
+ end
@@ -0,0 +1,33 @@
1
+ module TimeToWordsHelper
2
+ def time_to_words_ago(time)
3
+ a = (Time.now-time).to_i
4
+ case a
5
+ when 0 then 'just now'
6
+ when 1 then 'a second ago'
7
+ when 2..59 then a.to_s+' seconds ago'
8
+ when 60..119 then 'a minute ago' # 120 = 2 minutes
9
+ when 120..3540 then (a/60).to_i.to_s+' minutes ago'
10
+ when 3541..7100 then 'an hour ago' # 3600 = 1 hour
11
+ when 7101..82800 then ((a+99)/3600).to_i.to_s+' hours ago'
12
+ when 82801..172000 then 'a day ago' # 86400 = 1 day
13
+ when 172001..518400 then ((a+800)/(60*60*24)).to_i.to_s+' days ago'
14
+ when 518400..1036800 then 'a week ago'
15
+ else ((a+180000)/(60*60*24*7)).to_i.to_s+' weeks ago'
16
+ end
17
+ end
18
+
19
+ def time_to_words(seconds)
20
+ seconds
21
+ case a
22
+ when 0..60 then 'seconds'
23
+ when 60..69 then 'minute'
24
+ when 70..3600 then 'minutes'
25
+ when 3600..7199 then 'day'
26
+ when 7200..107999 then 'days' # 3600 = 1 hour
27
+ when 108000..215999 then 'month'
28
+ when 216000..1296000 then 'months'
29
+ when 1296000..1296000 then 'year'
30
+ else 'years'
31
+ end
32
+ end
33
+ end