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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 22377c5c1f038237e3b5999f1ceaf12a7b5df7e7
4
+ data.tar.gz: ab756e60d54c8b8cc12b8cc62ff9f48f113000f6
5
+ SHA512:
6
+ metadata.gz: d01528ded70155208de1635016846bca56e6e859b88d03676eaff2870725831be54b85e95355fa3e836164bbc98bf7eb5043905c6296519fd36b48f95044ef79
7
+ data.tar.gz: 33a19b730e74c339d7f42706ce2c201e7c9ebf0474c32a415035b7559a782870ab05e95c3157a4f24ae1f70ac04b893a31502338b3502589a838fdd0ab4e4c98
checksums.yaml.gz.sig ADDED
Binary file
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ Thumbs.db
3
+ *.csv
4
+ *.db
5
+ *.gem
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "thor", "~> 0.19.1"
4
+ gem "activerecord", "~> 4.1.6"
5
+ gem "sqlite3", "~> 1.3.9"
6
+ gem "chronic_duration", "0.10.6"
data/Gemfile.lock ADDED
@@ -0,0 +1,40 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ activemodel (4.1.10)
5
+ activesupport (= 4.1.10)
6
+ builder (~> 3.1)
7
+ activerecord (4.1.10)
8
+ activemodel (= 4.1.10)
9
+ activesupport (= 4.1.10)
10
+ arel (~> 5.0.0)
11
+ activesupport (4.1.10)
12
+ i18n (~> 0.6, >= 0.6.9)
13
+ json (~> 1.7, >= 1.7.7)
14
+ minitest (~> 5.1)
15
+ thread_safe (~> 0.1)
16
+ tzinfo (~> 1.1)
17
+ arel (5.0.1.20140414130214)
18
+ builder (3.2.2)
19
+ chronic (0.10.2)
20
+ chronic_duration (0.10.6)
21
+ numerizer (~> 0.1.1)
22
+ i18n (0.7.0)
23
+ json (1.8.2)
24
+ minitest (5.6.1)
25
+ numerizer (0.1.1)
26
+ sqlite3 (1.3.10)
27
+ thor (0.19.1)
28
+ thread_safe (0.3.5)
29
+ tzinfo (1.2.2)
30
+ thread_safe (~> 0.1)
31
+
32
+ PLATFORMS
33
+ ruby
34
+
35
+ DEPENDENCIES
36
+ activerecord (~> 4.1.6)
37
+ chronic (= 0.10.2)
38
+ chronic_duration (= 0.10.6)
39
+ sqlite3 (~> 1.3.9)
40
+ thor (~> 0.19.1)
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Sam 'Paperback' Sweeney
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.2
data/bin/logtime ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'logtime'
3
+ LogTime.start(ARGV)
@@ -0,0 +1,21 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDZDCCAkygAwIBAgIBATANBgkqhkiG9w0BAQUFADA8MQwwCgYDVQQDDANzYW0x
3
+ FzAVBgoJkiaJk/IsZAEZFgdzYW13aGF0MRMwEQYKCZImiZPyLGQBGRYDY29tMB4X
4
+ DTE1MDYwMjA5NTQxOVoXDTE2MDYwMTA5NTQxOVowPDEMMAoGA1UEAwwDc2FtMRcw
5
+ FQYKCZImiZPyLGQBGRYHc2Ftd2hhdDETMBEGCgmSJomT8ixkARkWA2NvbTCCASIw
6
+ DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPKKZV2uNAV6W+IlPDriPQNMPp7C
7
+ Xw7yNlbhmrmgNxJotT4DP6Eyc1NzAJdfz5kQMqgrzxRWBbFD1wPFJB3IdgX1PGKi
8
+ VOV/BO6sFBFrje/2a364FtKkGjm5V+zyeNZO/DUMuFalnbs+OWvfktFrx2JdBWeU
9
+ Utmtx3+AcW91+OFZ8y7ZZjUd1FbNP6A+WTYh2odUDFCQXe4xk3bcSGa0Ca7uAfv8
10
+ VMm1cWu23n+cer1dUW0VspFprSoo/f1WGduZus8PjBFAgEQdDDmTbB49DrigilzT
11
+ swXvrzGfCKZ0OveVFmhJz1Zr9WUlFNs/GU5x2iQpIZjg1S1mbIfhRhw+H1MCAwEA
12
+ AaNxMG8wCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFOGu79hFpVdT
13
+ ALi15MDDorfHC7nPMBoGA1UdEQQTMBGBD3NhbUBzYW13aGF0LmNvbTAaBgNVHRIE
14
+ EzARgQ9zYW1Ac2Ftd2hhdC5jb20wDQYJKoZIhvcNAQEFBQADggEBAIQ69hz8tXeJ
15
+ YPTz23tOss7qcY9xo5HInXQXfQ8wZGNVaBPmtBognGO/Kqf5PxeoaZ3FfT8pE9qz
16
+ giuth+pA2j2EXCBwEuu6+I4J9lfIT030uVMaBIQ1JJxKZcXtRpOP2yd/gKodDVn/
17
+ KIGKFrb3RWI2VVKNYV7lBCtHyx4uhEBvHmOhHKm3vlkzc+IZWQtCHhJ9iSKSefZ+
18
+ fZbMJQmYpt4/9KimNcpD+k3mQwMEW947060mRhnpAGkwlIujltb5lQwlWQoKm/Z4
19
+ 2/luIWFCIDih/t3xy/BezkXJLXfRZF2Xd6/Ax9+zEVss6iUJ6kcxwaZ/J5r28EDI
20
+ KS3vMuAGgck=
21
+ -----END CERTIFICATE-----
@@ -0,0 +1,6 @@
1
+ # Application configutation
2
+ I18n.config.enforce_available_locales = true
3
+ Time.zone = 'Brisbane'
4
+ ActiveRecord::Base.default_timezone = :local
5
+ ActiveRecord::Base.logger = Logger.new(STDERR)
6
+ ActiveRecord::Base.logger.level = 1
@@ -0,0 +1,4 @@
1
+ ActiveRecord::Base.establish_connection(
2
+ :adapter => "sqlite3",
3
+ :database => File.join($config_path, 'database.db') # or otherwise ":memory:"
4
+ )
@@ -0,0 +1,8 @@
1
+ require 'thor'
2
+ require 'active_record'
3
+ require 'sqlite3'
4
+ require 'chronic_duration'
5
+ $config_path = File.join(File.expand_path('~'), '.logtime')
6
+ require_relative 'database.rb'
7
+ require_relative 'application.rb'
8
+ require_relative 'schema.rb'
data/config/schema.rb ADDED
@@ -0,0 +1,41 @@
1
+ ActiveRecord::Schema.define do
2
+
3
+ #
4
+ # Logs
5
+ #
6
+ create_table :logs do |table|
7
+ table.column :name, :string
8
+ table.column :active, :string, default: false
9
+ table.column :estimation, :decimal
10
+ table.timestamps
11
+ end unless ActiveRecord::Base.connection.table_exists? 'logs'
12
+
13
+
14
+ #
15
+ # Logs Tags
16
+ #
17
+ create_table :logs_tags do |table|
18
+ table.column :log_id, :integer
19
+ table.column :tag_id, :integer
20
+ end unless ActiveRecord::Base.connection.table_exists? 'logs_tags'
21
+
22
+
23
+ #
24
+ # Tags
25
+ #
26
+ create_table :tags do |table|
27
+ table.column :tag, :string
28
+ table.timestamps
29
+ end unless ActiveRecord::Base.connection.table_exists? 'tags'
30
+
31
+ #
32
+ # Series
33
+ #
34
+ create_table :series do |table|
35
+ table.column :log_id, :integer
36
+ table.column :start, :timestamp
37
+ table.column :end, :timestamp
38
+ table.timestamps
39
+ end unless ActiveRecord::Base.connection.table_exists? 'series'
40
+
41
+ end unless File.exists?(File.join($config_path, 'database.db'))
@@ -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,56 @@
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 => :boolean, :alias => '-t'
7
+ desc "ls", "list time series"
8
+ def ls()
9
+ logs = Log.where(active: options[:active]).includes(:series)
10
+ # logs = Tag.where(tag: options[:tag]).logs if options[:tag]
11
+
12
+
13
+ creep_stats = Statistics.new
14
+ table = [['#', 'name', 'active', 'tags', 'series', 'time', 'estimate', 'creep %']] # header
15
+ logs.each do |log|
16
+
17
+ # tags
18
+ tags = log.tags.map do |t|
19
+ t.tag
20
+ end
21
+
22
+ # active text
23
+ active = "ACTIVE" if log.active == "t"
24
+
25
+ # total counter
26
+ total_time = (log.total/3600).round(2)
27
+
28
+ # estimation creep
29
+ if log.estimation
30
+ creep = (total_time/log.estimation)*100
31
+ creep_stats << creep
32
+ creep = creep.round(3)
33
+ estimation = (log.estimation/3600).round(3) # log estimation to hours
34
+ end
35
+
36
+ table << [log.id,
37
+ log.name,
38
+ active || '',
39
+ tags,
40
+ log.series.count || '',
41
+ total_time || '',
42
+ estimation || '',
43
+ creep || '']
44
+ end
45
+ puts ""
46
+ print_table table
47
+ puts ""
48
+
49
+ say [logs.count.to_s, "logs out of", Log.count.to_s].join(' '), :cyan
50
+ say [creep_stats.count, "estimations"].join(' '), :cyan
51
+ say [creep_stats.mean.round(3), "avg creep"].join(' '), :cyan
52
+ end
53
+ end
54
+ end
55
+ end
56
+ 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,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,45 @@
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
+ sum / size
9
+ end
10
+
11
+ alias :average :mean
12
+ end
13
+
14
+ class Statistics
15
+ def initialize
16
+ @statistics = Series.new
17
+ end
18
+
19
+ def add(statistic)
20
+ @statistics << statistic
21
+ end
22
+
23
+ def count
24
+ @statistics.count
25
+ end
26
+
27
+ def mean
28
+ @statistics.mean
29
+ end
30
+
31
+ def <<(statistic)
32
+ add(statistic)
33
+ end
34
+
35
+ def +(statistics)
36
+ @statistics = @statistics + statistics.data
37
+ end
38
+
39
+ def data
40
+ @statistics
41
+ end
42
+
43
+ alias :average :mean
44
+ end
45
+ 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
@@ -0,0 +1,67 @@
1
+ require '../config/initialisation'
2
+
3
+ # require models
4
+ models_path = '../app/models'
5
+ if Dir.exists?(models_path)
6
+ Dir.entries(models_path).each do |file|
7
+ require File.join(models_path, file) if file =~ /^(.*)\.rb$/
8
+ end
9
+ end
10
+
11
+ # require helpers
12
+ helpers_path = '../app/helpers'
13
+ if Dir.exists?(helpers_path)
14
+ Dir.entries(helpers_path).each do |file|
15
+ next unless file =~ /^(.*)\.rb$/
16
+
17
+ require File.join(helpers_path, file)
18
+
19
+ # helper name
20
+ load = file.split '_'
21
+ load = load.each { |h| h.capitalize! }
22
+ load = load.join
23
+ load = load[0..-4] # '.rb'
24
+ load = load + "Helper"
25
+
26
+ # load helper
27
+ # begin
28
+ send(:include, load.constantize)
29
+ # rescue
30
+ # ActiveRecord::Base.logger.warn "Failed to load " + load
31
+ # end
32
+ end
33
+ end
34
+
35
+ # require commands
36
+ commands_path = '../app/commands'
37
+ if Dir.exists?(commands_path)
38
+ Dir.entries(commands_path).each do |file|
39
+ require File.join(commands_path, file) if file =~ /^(.*)\.rb$/
40
+ end
41
+ end
42
+
43
+ # require subcommands
44
+ subcommands_path = '../app/subcommands'
45
+ if Dir.exists?(subcommands_path)
46
+ Dir.entries(subcommands_path).each do |file|
47
+ require File.join(subcommands_path, file) if file =~ /^(.*)\.rb$/
48
+ end
49
+ end
50
+
51
+ # Application
52
+ class LogTime < Thor
53
+
54
+ # commands
55
+ include Commands::List
56
+ include Commands::Add
57
+ # include Commands::Remove
58
+ include Commands::Start
59
+ include Commands::Stop
60
+
61
+ # subcommands
62
+ desc "series", ""
63
+ subcommand "series", SubCommands::Series
64
+
65
+ desc "tags", ""
66
+ subcommand "tags", SubCommands::Tags
67
+ end
@@ -0,0 +1,42 @@
1
+ #
2
+ # Logs
3
+ #
4
+
5
+ class Log < ActiveRecord::Base
6
+ scope :active, -> { where(active: true) }
7
+ scope :inactive, -> { where(active: false) }
8
+ scope :estimated, -> { where("estimation is not null") }
9
+
10
+ validates :name, uniqueness: true
11
+
12
+ has_many :series
13
+ has_and_belongs_to_many :tags
14
+
15
+ def tag(tags)
16
+ tags.split(',').each do |t|
17
+ self.tags << Tag.where(tag: t).first_or_create
18
+ end
19
+
20
+ return self
21
+ end
22
+
23
+ def activate
24
+ self.active = true
25
+ self.save
26
+ end
27
+
28
+ def deactivate
29
+ self.active = false
30
+ self.save
31
+ end
32
+
33
+ def total
34
+ difference = 0
35
+ self.series.each do |s|
36
+ ending = s.end || Time.now
37
+ # next unless s.start and s.end
38
+ difference = difference + s.start.difference(ending)
39
+ end
40
+ difference
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ #
2
+ # Series
3
+ #
4
+
5
+ class Series < ActiveRecord::Base
6
+ belongs_to :log
7
+
8
+ def self.begin(options = {})
9
+ attributes = {
10
+ start: Time.now
11
+ }.merge! options
12
+
13
+ create(attributes)
14
+ end
15
+
16
+ def begin
17
+ self.start = Time.now
18
+ self.save
19
+ end
20
+
21
+ def stop
22
+ self.end = Time.now
23
+ self.save
24
+ end
25
+
26
+ def finished?
27
+ !self.end.blank?
28
+ end
29
+
30
+ def total
31
+ return false if self.end.blank? or self.start.blank?
32
+
33
+ self.start.difference(self.end)
34
+ end
35
+ end
@@ -0,0 +1,9 @@
1
+ #
2
+ # Tags
3
+ #
4
+
5
+ class Tag < ActiveRecord::Base
6
+ validates :tag, uniqueness: true
7
+ validates_format_of :tag, :with => /[-a-z]+/
8
+ has_and_belongs_to_many :logs
9
+ end