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
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