logtime 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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