timelog 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ dev
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in timelog.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Sune Kibsgaard Pedersen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ # Timelog
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'timelog'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install timelog
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ lib = File.expand_path(File.dirname(__FILE__) + '/../lib')
3
+ $LOAD_PATH.unshift(lib) if File.directory?(lib) && !$LOAD_PATH.include?(lib)
4
+
5
+ require 'timelog'
6
+
7
+ debug = ENV['TIMELOG_DEBUG'] || ENV['timelog_debug']
8
+ config = ENV['TIMELOG_CONFIG'] || ENV['timelog_config'] || "#{Dir.home}/.timelog.rb"
9
+
10
+ begin
11
+ Timelog::Book.open(config) do
12
+ puts execute ARGV
13
+ end
14
+ rescue Timelog::Error => e
15
+ abort e.message
16
+ rescue e
17
+ if debug
18
+ raise e
19
+ else
20
+ abort "an unexpected error occurred"
21
+ end
22
+ end
@@ -0,0 +1,41 @@
1
+ require 'active_support/all'
2
+ require 'active_model'
3
+ require 'timelog/version'
4
+ require 'timelog/database'
5
+ require 'timelog/book'
6
+ require 'timelog/configuration'
7
+ require 'timelog/entry'
8
+ require 'timelog/command'
9
+
10
+ module Timelog
11
+ class Error < StandardError
12
+ def initialize(msg = "unknown timelog error occurred")
13
+ super(msg)
14
+ end
15
+ end
16
+
17
+ def self.create_error(name, default_message)
18
+ c = Class.new(Error) do |msg|
19
+ define_method :initialize do |msg = default_message|
20
+ super(msg)
21
+ end
22
+ end
23
+ Kernel.const_set name.camelcase, c
24
+ end
25
+
26
+ create_error 'invalid_argument_error', 'invalid arguments'
27
+ create_error 'invalid_command_error', 'invalid command'
28
+ create_error 'invalid_entry', 'invalid entry'
29
+ create_error 'invalid_query_error', 'invalid query'
30
+ create_error 'entry_started_error', 'entry already started'
31
+ create_error 'entry_paused_error', 'entry already paused'
32
+ create_error 'entry_not_active_error', 'entry not active'
33
+ create_error 'entry_not_paused_error', 'entry is not paused'
34
+ create_error 'entry_not_started_error', 'entry is not started'
35
+ create_error 'client_exists_error', 'client already exists'
36
+ create_error 'client_not_found_error', 'client was not found'
37
+ create_error 'missing_client_name_error', 'missing client name'
38
+ create_error 'project_exists_error', 'project already exists'
39
+ create_error 'project_not_found_error', 'project was not found'
40
+ create_error 'missing_project_name_error', 'missing project name'
41
+ end
@@ -0,0 +1,91 @@
1
+ require 'timelog/book/entries'
2
+ require 'timelog/book/clients'
3
+ require 'timelog/book/projects'
4
+
5
+ module Timelog
6
+ class Book
7
+ attr_reader :configuration, :database
8
+
9
+ # initialize a book
10
+ #
11
+ # using a configuration object.
12
+ # have a look at the .open method for
13
+ # initializing a book from a configuration file
14
+ def initialize(configuration)
15
+ @configuration = configuration
16
+ @database = Database.new(@configuration)
17
+ @entries = Entries.new(self)
18
+ @clients = Clients.new(self)
19
+ @projects = Projects.new(self)
20
+ end
21
+
22
+ # execute a command
23
+ #
24
+ # this finds and executes a command given from a string
25
+ # or an array of arguments
26
+ def execute(arguments)
27
+ arguments = arguments.split if arguments.is_a? String
28
+ # we have arguments, so try to find the command
29
+ if arguments.size > 0
30
+ name = ""
31
+ # add one argument at a time, until (or never) a command matches
32
+ command = while(argument = arguments.shift)
33
+ name += " #{argument}"
34
+ if obj = Command.get(name.strip, self, arguments)
35
+ break obj
36
+ end
37
+ end
38
+ # no arguments, use generic command
39
+ else
40
+ command = Command.new(self)
41
+ end
42
+ raise InvalidCommandError unless command
43
+
44
+ # looking for command help? no? then execute!
45
+ arguments.first.try(:downcase) == "help" ? command.help : command.execute
46
+ end
47
+
48
+ # trigger an event
49
+ def trigger(group, event, *args)
50
+ case group
51
+ when :before
52
+ @configuration.callbacks[group][event].each {|block| instance_exec(*args, &block)} if @configuration.callbacks[group][event]
53
+ when :after
54
+ @configuration.callbacks[group][event].each {|block| instance_exec(*args, &block)} if @configuration.callbacks[group][event]
55
+ end
56
+ end
57
+
58
+ # get the entries controller
59
+ #
60
+ # use this for manipulate entries in the book
61
+ # if a block is given, it will be evalulated in the
62
+ # controllers context
63
+ def entries(&block)
64
+ @entries.instance_eval(&block) if block_given?
65
+ @entries
66
+ end
67
+
68
+ def clients(&block)
69
+ @clients.instance_eval(&block) if block_given?
70
+ @clients
71
+ end
72
+
73
+ def projects(&block)
74
+ @projects.instance_eval(&block) if block_given?
75
+ @projects
76
+ end
77
+
78
+ class << self
79
+ # open an existing book
80
+ #
81
+ # using a configuration file and an
82
+ # optional block which will be evaluated
83
+ # in the book instance
84
+ def open(file, &block)
85
+ book = Book.new(Configuration.open(file))
86
+ book.instance_eval(&block) if block_given?
87
+ book
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,29 @@
1
+ module Timelog
2
+ class Book
3
+ class Clients
4
+ def initialize(book)
5
+ @book = book
6
+ end
7
+
8
+ def add(name, shortcut = nil)
9
+ raise ClientExistsError if exists? name or (shortcut and exists? shortcut)
10
+ @book.database.execute("insert into clients (name, shortcut) values(?, ?)", [name, shortcut])
11
+ @book.database.first_value("select id from clients order by id desc limit 1")
12
+ end
13
+
14
+ def exists?(client)
15
+ @book.database.first_value("select id from clients where id = ? or name = ? or shortcut = ?", [client, client, client])
16
+ end
17
+
18
+ def name(client)
19
+ raise ClientNotFoundError unless exists? client
20
+ @book.database.first_value("select name from clients where id = ? or name = ? or shortcut = ?", [client, client, client])
21
+ end
22
+
23
+ def id(client)
24
+ raise ClientNotFoundError unless exists? client
25
+ @book.database.first_value("select id from clients where id = ? or name = ? or shortcut = ?", [client, client, client])
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,151 @@
1
+ module Timelog
2
+ class Book
3
+ class Entries
4
+ QUERY_REGEX = /\A\d+|all|archived|non\-archived|active|today\Z/
5
+
6
+ def initialize(book)
7
+ @book = book
8
+ end
9
+
10
+ # pauses an entry
11
+ def pause(entry, options = {})
12
+ raise EntryNotActiveError unless entry.active?
13
+ raise EntryPausedError if entry.paused?
14
+ raise EntryNotStarted unless entry.started?
15
+ @book.trigger :before, :pause, entry
16
+ entry.pause options[:at]
17
+ write entry
18
+ @book.trigger :after, :pause, entry
19
+ end
20
+
21
+ # resumes an entry
22
+ def resume(entry, options = {})
23
+ raise EntryNotActiveError unless entry.active?
24
+ raise EntryNotPausedError unless entry.paused?
25
+ @book.trigger :before, :resume, entry
26
+ entry.resume options[:at]
27
+ write entry
28
+ @book.trigger :after, :resume, entry
29
+ end
30
+
31
+ # starts an entry
32
+ def start(entry, options = {})
33
+ raise EntryStartedError if entry.started?
34
+ @book.trigger :before, :start, entry
35
+ entry.started = options[:at] || Time.now
36
+ write entry
37
+ @book.trigger :after, :start, entry
38
+ end
39
+
40
+ # ends the entry
41
+ def stop(entry, options = {})
42
+ raise EntryNotStartedError unless entry.started?
43
+ @book.trigger :before, :stop, entry
44
+ entry.resume(options[:at] || Time.now) if entry.paused?
45
+ entry.stopped = options[:at] || Time.now
46
+ write entry
47
+ @book.trigger :after, :stop, entry
48
+ end
49
+
50
+ # archives the entry
51
+ def archive(entry, options = {})
52
+ @book.trigger :before, :archive, entry
53
+ entry.archived = true
54
+ write entry
55
+ @book.trigger :after, :archive, entry
56
+ end
57
+
58
+ # get the active or a new entry
59
+ #
60
+ # this can be an already started entry
61
+ # or a fresh nonstarted entry if an ongoing
62
+ # entry was not found
63
+ def active_or_new
64
+ active || Entry.new
65
+ end
66
+
67
+ # get the active entry
68
+ #
69
+ # returns the currently active entry
70
+ # or nil if none was found
71
+ def active
72
+ where("active").first
73
+ end
74
+
75
+ # find entries from query
76
+ #
77
+ # get a list of entries, based on the query provided
78
+ def where(query)
79
+ raise InvalidQueryError if query !~ QUERY_REGEX
80
+ bind = []
81
+ where = case query.to_s
82
+ when /\A\d+\Z/
83
+ bind << query
84
+ "where id = ?"
85
+ when "all" then ""
86
+ when "active" then "where stopped is null"
87
+ when "archived" then "where archived = 1"
88
+ when "non-archived" then "where archived = 0"
89
+ when "today"
90
+ bind += [Date.today.to_time.to_i, Date.today.to_time.to_i + 24 * 60 * 60]
91
+ "where started > ? and started < ?"
92
+ end
93
+ read(@book.database.first_column("select id from entries #{where} order by id", *bind))
94
+ end
95
+
96
+ # get entries from ids
97
+ #
98
+ # fetches and loads all entries with the given ids
99
+ # returns an array with entries sorted by id desc
100
+ def read(id)
101
+ id = [id] unless id.is_a? Array
102
+ @book.database.execute("select * from entries where id in (#{id.compact.map {"?"}.join(", ")}) order by id", id.compact).map do |row|
103
+ entry = Entry.new
104
+ entry.id = row[0]
105
+ entry.client = row[1]
106
+ entry.project = row[2]
107
+ entry.description = row[3]
108
+ entry.started = Time.at(row[4]) if row[4]
109
+ entry.stopped = Time.at(row[5]) if row[5]
110
+ if row[6] and pauses = Marshal.load(row[6])
111
+ pauses.each {|pause, resume| entry.pause(pause); entry.resume(resume)}
112
+ end
113
+ entry.archived = (row[7] == 1) ? true : false
114
+ entry
115
+ end
116
+ end
117
+
118
+ # write a single entry to book
119
+ #
120
+ # persists an entry to the database
121
+ # automatically sets the id on the entry
122
+ # if it is a new entry
123
+ def write(entry)
124
+ if entry.valid?
125
+ data = [
126
+ entry.client,
127
+ entry.project,
128
+ entry.description,
129
+ entry.started.try(:to_i),
130
+ entry.stopped.try(:to_i),
131
+ Marshal.dump(entry.pauses),
132
+ entry.archived ? 1 : 0
133
+ ]
134
+ if entry.id
135
+ @book.database.execute("update entries set
136
+ client = ?, project = ?, description = ?,
137
+ started = ?, stopped = ?, pauses = ?,
138
+ archived = ? where id = ?", data << entry.id)
139
+ else
140
+ @book.database.execute("insert into entries
141
+ (client, project, description, started, stopped, pauses, archived)
142
+ values(?, ?, ?, ?, ?, ?, ?)", data)
143
+ entry.id = @book.database.first_value("select id from entries order by id desc limit 1")
144
+ end
145
+ else
146
+ raise InvalidEntryError
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,33 @@
1
+ module Timelog
2
+ class Book
3
+ class Projects
4
+ def initialize(book)
5
+ @book = book
6
+ end
7
+
8
+ def add(client, project, shortcut = nil)
9
+ raise ProjectExistsError if exists?(client, project) or (shortcut and exists?(client, shortcut))
10
+ client = @book.clients.exists? client
11
+ @book.database.execute("insert into projects (client, name, shortcut) values(?, ?, ?)", [client, project, shortcut])
12
+ @book.database.first_value("select id from projects order by id desc limit 1")
13
+ end
14
+
15
+ def exists?(client, project)
16
+ raise ClientNotFoundError unless client = @book.clients.exists?(client)
17
+ @book.database.first_value("select id from projects where client = ? and (id = ? or name = ? or shortcut = ?)", [client, project, project, project])
18
+ end
19
+
20
+ def name(client, project)
21
+ client = @book.clients.id(client)
22
+ raise ProjectNotFoundError unless exists? client, project
23
+ @book.database.first_value("select name from projects where client = ? and (id = ? or name = ? or shortcut = ?)", [client, project, project, project])
24
+ end
25
+
26
+ def id(client, project)
27
+ client = @book.clients.id(client)
28
+ raise ProjectNotFoundError unless exists? client, project
29
+ @book.database.first_value("select id from projects where client = ? and (id = ? or name = ? or shortcut = ?)", [client, project, project, project])
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ module Timelog
2
+ class Command
3
+ attr_reader :book, :session, :arguments
4
+ @commands = {}
5
+
6
+ def initialize(book, arguments = [])
7
+ @book = book
8
+ @session = session
9
+ @arguments = arguments
10
+ end
11
+
12
+ def execute
13
+ help
14
+ end
15
+
16
+ def help
17
+ <<-HELP
18
+ usage: timelog <command> [arguments]
19
+
20
+ Commands:
21
+ #{Command.known.join("\n ")}
22
+
23
+ See 'timelog <command> help' for more information on a specific command.
24
+ HELP
25
+ end
26
+
27
+ def time_in_arguments(default = Time.now)
28
+ if time = @arguments.find {|arg| arg.match /\A([01]?[0-9]|2[0-3])([\:\.]([0-5][0-9])|)\Z/}
29
+ hour = time.match(/^([01]?[0-9]|2[0-3])/)[0].to_i
30
+ minute = time.match(/([\:\.]([0-5][0-9]))$/).try(:values_at, 2).try(:first).try(:to_i) || 0
31
+ @arguments.delete_if {|arg| arg =~ TIME_REGEX}
32
+ Time.now.change({hour: hour, min: minute, sec: 0})
33
+ end
34
+ default
35
+ end
36
+
37
+ class << self
38
+ def inherited(subclass)
39
+ register subclass.name.split("::").last.underscore.gsub("_"," "), subclass
40
+ end
41
+
42
+ def register(name, klass)
43
+ @commands[name] = klass
44
+ end
45
+
46
+ def get(name, book, arguments = [])
47
+ klass = @commands[name]
48
+ klass.new(book, arguments) if klass
49
+ end
50
+
51
+ def known
52
+ @commands.keys
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # register all commands
59
+ Dir[File.dirname(__FILE__) + '/command/*.rb'].each {|file| require file }
@@ -0,0 +1,20 @@
1
+ module Timelog
2
+ class Command
3
+ class Active < Command
4
+ def execute
5
+ if entry = book.entries.active
6
+ client = (entry.client and book.clients.exists?(entry.client)) ? book.clients.name(entry.client) : nil
7
+ project = (client and entry.project and book.projects.exists?(client, entry.project)) ? book.projects.name(client, entry.project) : nil
8
+ description = "%s%s" % [client || "n/a", project ? ": #{project}" : ""]
9
+ "%s - %02d:%02d:%02d#{" (paused)" if entry.paused?}" % ([description] + entry.active_time.values)
10
+ else
11
+ "no active entry"
12
+ end
13
+ end
14
+
15
+ def help
16
+ "usage: timelog active"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ module Timelog
2
+ class Command
3
+ class Archive < Command
4
+ def execute
5
+ entries = book.entries.where(arguments.first || "non-archived")
6
+ entries.delete_if {|e| not e.stopped? or e.archived}
7
+ if entries.size == 0
8
+ "no entries found for archiving"
9
+ else
10
+ entries.each do |e|
11
+ book.entries.archive e
12
+ end
13
+ "#{entries.length} #{entries.length == 1 ? "entry" : "entries"} archived"
14
+ end
15
+ end
16
+
17
+ def help
18
+ "usage: timelog archive [<query>]"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ module Timelog
2
+ class Command
3
+ class ClientAdd < Command
4
+ def execute
5
+ raise InvalidArgumentError if arguments.size > 2
6
+ raise MissingClientNameError unless name = arguments.shift
7
+
8
+ book.clients.add name, arguments.shift
9
+ "client #{name} added"
10
+ end
11
+
12
+ def help
13
+ "usage: timelog client add <name> [<shortcut>]"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ module Timelog
2
+ class Command
3
+ # just to make sure the the global help is shown if help is put as an command
4
+ class Help < Command; end
5
+ end
6
+ end
@@ -0,0 +1,64 @@
1
+ module Timelog
2
+ class Command
3
+ class List < Command
4
+ def execute
5
+ table = book.entries.where(arguments.first || "non-archived").map do |e|
6
+ {
7
+ id: e.id,
8
+ client: e.client ? book.clients.name(e.client) : "",
9
+ project: e.project ? book.projects.name(e.client, e.project) : "",
10
+ description: e.description,
11
+ started: e.started.try(:strftime, "%Y-%m-%d %H:%M"),
12
+ stopped: e.stopped.try(:strftime, "%Y-%m-%d %H:%M"),
13
+ "active time" => "%02d:%02d:%02d" % e.active_time.values,
14
+ "paused time" => e.paused_seconds > 0 ? "%02d:%02d:%02d" % e.paused_time.values : "",
15
+ "total time" => "%02d:%02d:%02d" % e.total_time.values,
16
+ status: e.status
17
+ }
18
+ end
19
+
20
+ if table.size == 0
21
+ "no entries found"
22
+ else
23
+ sizes = column_sizes_for table
24
+ width = sizes.values.reduce(:+) + sizes.length * 3 + 1
25
+
26
+ separater = ''
27
+ width.times { separater << '-' }
28
+
29
+ lines = [separater]
30
+ lines << "|" + sizes.map do |name, length|
31
+ " %-#{length}s " % name
32
+ end.join("|") + "|"
33
+ lines << separater
34
+
35
+ table.each do |row|
36
+ line = []
37
+ row.each do |key, value|
38
+ line << " %-#{sizes[key]}s " % value
39
+ end
40
+ lines << "|" + line.join("|") + "|"
41
+ end
42
+ lines << separater
43
+
44
+ lines.join("\n")
45
+ end
46
+ end
47
+
48
+ def help
49
+ "usage: timelog list [<query>]"
50
+ end
51
+
52
+ def column_sizes_for(table)
53
+ columns = {}
54
+ table.each do |row|
55
+ row.each do |key, value|
56
+ columns[key] ||= key.size
57
+ columns[key] = value.to_s.size if value and value.to_s.size > columns[key]
58
+ end
59
+ end
60
+ columns
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,15 @@
1
+ module Timelog
2
+ class Command
3
+ class Pause < Command
4
+ def execute
5
+ entry = book.entries.active_or_new
6
+ book.entries.pause entry, at: time_in_arguments
7
+ "entry paused"
8
+ end
9
+
10
+ def help
11
+ "usage: timelog pause [<time>]"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ module Timelog
2
+ class Command
3
+ class ProjectAdd < Command
4
+ def execute
5
+ raise InvalidArgumentError if arguments.size > 3
6
+ raise MissingClientNameError unless client = arguments.shift
7
+ raise MissingProjectNameError unless name = arguments.shift
8
+
9
+ book.projects.add client, name, arguments.shift
10
+ "project #{name} added"
11
+ end
12
+
13
+ def help
14
+ "usage: timelog project add <client> <name> [<shortcut>]"
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module Timelog
2
+ class Command
3
+ class Resume < Command
4
+ def execute
5
+ entry = book.entries.active_or_new
6
+ book.entries.resume entry, at: time_in_arguments
7
+ "entry resumed"
8
+ end
9
+
10
+ def help
11
+ "usage: timelog resume [<time>]"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module Timelog
2
+ class Command
3
+ class Start < Command
4
+ def execute
5
+ time = time_in_arguments
6
+ raise InvalidArgumentError if arguments.size > 2
7
+
8
+ client = arguments.shift
9
+ project = arguments.shift
10
+
11
+ client = book.clients.id(client) unless client.nil?
12
+ project = book.projects.id(client, project) unless project.nil?
13
+
14
+ entry = book.entries.active_or_new
15
+ entry.client = client
16
+ entry.project = project
17
+
18
+ book.entries.start entry, at: time
19
+ "entry started"
20
+ end
21
+
22
+ def help
23
+ "usage: timelog start [<client>] [<project>] [<time>]"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module Timelog
2
+ class Command
3
+ class Stop < Command
4
+ def execute
5
+ time = time_in_arguments
6
+
7
+ entry = book.entries.active_or_new
8
+ entry.description = arguments.join(" ")
9
+
10
+ book.entries.stop entry, at: time
11
+ "entry stopped"
12
+ end
13
+
14
+ def help
15
+ "usage: timelog stop [<description>] [<time>]"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,36 @@
1
+ module Timelog
2
+ class Configuration
3
+ attr_accessor :database, :callbacks
4
+
5
+ def initialize
6
+ # defaults
7
+ @database = "#{Dir.home}/.timelog.db"
8
+ @callbacks = {before: {}, after: {}}
9
+ end
10
+
11
+ def set(name, value)
12
+ send("#{name}=", value)
13
+ end
14
+
15
+ def before(event, &block)
16
+ @callbacks[:before][event.to_sym] ||= []
17
+ @callbacks[:before][event.to_sym] << block if block_given?
18
+ @callbacks[:before][event.to_sym]
19
+ end
20
+
21
+ def after(event, &block)
22
+ @callbacks[:after][event.to_sym] ||= []
23
+ @callbacks[:after][event.to_sym] << block if block_given?
24
+ @callbacks[:after][event.to_sym]
25
+ end
26
+
27
+ class << self
28
+ def open(file = nil, &block)
29
+ instance = self.new
30
+ instance.instance_eval(File.read(file), file) if file and File.exists?(file)
31
+ instance.instance_eval(&block) if block_given?
32
+ instance
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,55 @@
1
+ require 'sqlite3'
2
+
3
+ module Timelog
4
+ class Database
5
+ def initialize(configuration)
6
+ @configuration = configuration
7
+ @database = SQLite3::Database.new @configuration.database
8
+ update_to_latest_version!
9
+ end
10
+
11
+ def execute(sql, bind_vars = [], *args, &block)
12
+ @database.execute(sql, bind_vars, *args, &block)
13
+ end
14
+
15
+ def first_row(sql, *bind_vars)
16
+ @database.get_first_row(sql, *bind_vars)
17
+ end
18
+
19
+ def first_value(sql, *bind_vars)
20
+ @database.get_first_value(sql, *bind_vars)
21
+ end
22
+
23
+ def first_column(sql, *bind_vars)
24
+ @database.execute(sql, *bind_vars).map {|row| row.first}
25
+ end
26
+
27
+ private
28
+
29
+ def update_to_latest_version!
30
+ # create version table (if not already exists)
31
+ @database.execute "create table if not exists version (number int)"
32
+
33
+ # get current version
34
+ current = @database.get_first_value("select number from version")
35
+ unless current
36
+ @database.execute "insert into version values(0)"
37
+ current = 0
38
+ end
39
+
40
+ versions = {
41
+ 1 => "create table entries (id integer primary key, client integer, project integer, description text, started integer, stopped integer)",
42
+ 2 => "alter table entries add column pauses text",
43
+ 3 => "alter table entries add column archived integer not null default 0",
44
+ 4 => "create table clients (id integer primary key, name text not null unique, shortcut text unique)",
45
+ 5 => "create table projects (id integer primary key, client integer not null, name text not null, shortcut text)"
46
+ }
47
+
48
+ # run version migrations
49
+ versions.each {|version, query| @database.execute(query) if current < version }
50
+
51
+ # update version
52
+ @database.execute("update version set number = ?", versions.keys.last)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,100 @@
1
+ module Timelog
2
+ class Entry
3
+ include ActiveModel::Validations
4
+
5
+ attr_accessor :id, :client, :project, :description
6
+ attr_reader :started, :stopped, :pauses, :archived
7
+
8
+ alias :archived? :archived
9
+
10
+ def initialize
11
+ @pauses = {}
12
+ @archived = false
13
+ end
14
+
15
+ def started=(at)
16
+ @started = at
17
+ end
18
+
19
+ def stopped=(at)
20
+ resume if paused?
21
+ @stopped = at
22
+ end
23
+
24
+ def started?
25
+ @started
26
+ end
27
+
28
+ def stopped?
29
+ @stopped
30
+ end
31
+
32
+ def pause(at = Time.now)
33
+ raise EntryAlreadyPaused if paused?
34
+ @pauses[at] = nil
35
+ end
36
+
37
+ def resume(at = Time.now)
38
+ raise EntryNotPaused unless paused?
39
+ @pauses[@pauses.keys.last] = at
40
+ end
41
+
42
+ def paused?
43
+ @pauses.length > 0 and @pauses.values.last.nil?
44
+ end
45
+
46
+ def date
47
+ started.try(:to_date)
48
+ end
49
+
50
+ def active_seconds
51
+ total_seconds - paused_seconds
52
+ end
53
+
54
+ def paused_seconds
55
+ pauses.inject(0) do |seconds, pause|
56
+ seconds + ((pause[1] || Time.now) - pause[0])
57
+ end.to_i
58
+ end
59
+
60
+ def total_seconds
61
+ return 0 unless started?
62
+ ((stopped || Time.now) - started).round
63
+ end
64
+
65
+ def active_time
66
+ parse_seconds(active_seconds)
67
+ end
68
+
69
+ def paused_time
70
+ parse_seconds(paused_seconds)
71
+ end
72
+
73
+ def total_time
74
+ parse_seconds(total_seconds)
75
+ end
76
+
77
+ def archived=(value)
78
+ @archived = value ? true : false
79
+ end
80
+
81
+ def status
82
+ return "archived" if archived?
83
+ return "paused" if paused?
84
+ return "active" if active?
85
+ end
86
+
87
+ def active?
88
+ !stopped?
89
+ end
90
+
91
+ private
92
+
93
+ def parse_seconds(seconds)
94
+ hours = seconds / 60 / 60
95
+ minutes = (seconds - hours * 60 * 60) / 60
96
+ seconds = (seconds - hours * 60 * 60) - (minutes * 60)
97
+ {hours: hours, minutes: minutes, seconds: seconds}
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module Timelog
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/timelog/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Sune Kibsgaard Pedersen"]
6
+ gem.email = ["sune@kibs.dk"]
7
+ gem.description = %q{Time logger}
8
+ gem.summary = %q{Time logger}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "timelog"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Timelog::VERSION
17
+ gem.add_dependency "sqlite3", "~> 1.3"
18
+ gem.add_dependency "activesupport", "= 3.2.11"
19
+ gem.add_dependency "activemodel", "= 3.2.11"
20
+ gem.add_development_dependency "pry"
21
+ end
metadata ADDED
@@ -0,0 +1,138 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: timelog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Sune Kibsgaard Pedersen
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sqlite3
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - '='
36
+ - !ruby/object:Gem::Version
37
+ version: 3.2.11
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - '='
44
+ - !ruby/object:Gem::Version
45
+ version: 3.2.11
46
+ - !ruby/object:Gem::Dependency
47
+ name: activemodel
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: 3.2.11
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 3.2.11
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Time logger
79
+ email:
80
+ - sune@kibs.dk
81
+ executables:
82
+ - timelog
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - Gemfile
88
+ - LICENSE
89
+ - README.md
90
+ - Rakefile
91
+ - bin/timelog
92
+ - lib/timelog.rb
93
+ - lib/timelog/book.rb
94
+ - lib/timelog/book/clients.rb
95
+ - lib/timelog/book/entries.rb
96
+ - lib/timelog/book/projects.rb
97
+ - lib/timelog/command.rb
98
+ - lib/timelog/command/active.rb
99
+ - lib/timelog/command/archive.rb
100
+ - lib/timelog/command/client_add.rb
101
+ - lib/timelog/command/help.rb
102
+ - lib/timelog/command/list.rb
103
+ - lib/timelog/command/pause.rb
104
+ - lib/timelog/command/project_add.rb
105
+ - lib/timelog/command/resume.rb
106
+ - lib/timelog/command/start.rb
107
+ - lib/timelog/command/stop.rb
108
+ - lib/timelog/configuration.rb
109
+ - lib/timelog/database.rb
110
+ - lib/timelog/entry.rb
111
+ - lib/timelog/version.rb
112
+ - timelog.gemspec
113
+ homepage: ''
114
+ licenses: []
115
+ post_install_message:
116
+ rdoc_options: []
117
+ require_paths:
118
+ - lib
119
+ required_ruby_version: !ruby/object:Gem::Requirement
120
+ none: false
121
+ requirements:
122
+ - - ! '>='
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ requirements: []
132
+ rubyforge_project:
133
+ rubygems_version: 1.8.24
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: Time logger
137
+ test_files: []
138
+ has_rdoc: