ptimelog 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e821e2d48cc867c15d85cff85ac979f69a9474b638af7b8964033dffb3539058
4
+ data.tar.gz: 8b9838927d489e8fcf7ac6d77cbd223218eb6e0c1c8463faafe1f3af95e89627
5
+ SHA512:
6
+ metadata.gz: 9de52fc86ab42309968391b811d64536071726bf48d139c3600fd836b2ea710bca7010d2f3be6750ddcfdf57ec0d42eb2a204c396ee469eaea432799a39030ef
7
+ data.tar.gz: 94516e9782ab52375ec8bc29aa20228575d396e3181652181efb5a6f5f4a80c4ca2be0423e350c859adefd8106dcf7ca08f1e39c6abf6561039fb78699931df6
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.overcommit.yml ADDED
@@ -0,0 +1,26 @@
1
+ # Use this file to configure the Overcommit hooks you wish to use. This will
2
+ # extend the default configuration defined in:
3
+ # https://github.com/brigade/overcommit/blob/master/config/default.yml
4
+ #
5
+ # For a complete list of hooks, see:
6
+ # https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
7
+ #
8
+ # For a complete list of options that you can use to customize hooks, see:
9
+ # https://github.com/brigade/overcommit#configuration
10
+
11
+ PreCommit:
12
+ RuboCop:
13
+ enabled: true
14
+ on_warn: fail # Treat all warnings as failures
15
+
16
+ TrailingWhitespace:
17
+ enabled: true
18
+ # exclude:
19
+ # - '**/db/structure.sql' # Ignore trailing whitespace in generated files
20
+
21
+ #PostCheckout:
22
+ # ALL: # Special hook name that customizes all hooks of this type
23
+ # quiet: true # Change all post-checkout hooks to only display output on failure
24
+ #
25
+ # IndexTags:
26
+ # enabled: true # Generate a tags file with `ctags` each time HEAD changes
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,33 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ Metrics/LineLength:
4
+ Max: 120
5
+
6
+ Metrics/BlockLength:
7
+ Exclude:
8
+ - spec/**/*
9
+
10
+ Style/TrailingCommaInArrayLiteral:
11
+ EnforcedStyleForMultiline: consistent_comma
12
+
13
+ Style/TrailingCommaInHashLiteral:
14
+ EnforcedStyleForMultiline: consistent_comma
15
+
16
+ Layout/AlignHash:
17
+ EnforcedHashRocketStyle: table
18
+ EnforcedColonStyle: table
19
+ EnforcedLastArgumentHashStyle: always_inspect # default
20
+
21
+ Naming/UncommunicativeMethodParamName:
22
+ AllowedNames:
23
+ - fn
24
+ # default allowed names
25
+ - io
26
+ - id
27
+ - to
28
+ - by
29
+ - on
30
+ - in
31
+ - at
32
+ - ip
33
+ - db
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,16 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2019-08-06 22:23:40 +0200 using RuboCop version 0.65.0.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 3
10
+ Metrics/AbcSize:
11
+ Max: 16
12
+
13
+ # Offense count: 5
14
+ # Configuration parameters: CountComments, ExcludedMethods.
15
+ Metrics/MethodLength:
16
+ Max: 15
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ gpuzzletime
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.3
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.1
4
+ - 2.4.2
5
+ - 2.5.3
6
+ - 2.6.1
7
+ before_install: gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in ptimelog.gemspec
6
+ gemspec name: 'ptimelog'
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017-2018 Matthias Viehweger
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # pTimeLog
2
+
3
+ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the PuzzleTime TimeTracking Website.
4
+
5
+ ## Approach
6
+
7
+ - [x] read timelog.txt
8
+ - [x] from known location
9
+ - [x] later: configure location?
10
+ - [ ] later: auto-detect location?
11
+ - [x] parse out last day
12
+ - [x] especially start/end-times for each entry
13
+ - [x] date - ticket - description
14
+ - [x] later: parse out specific day
15
+ - [x] open N browser instances with the time entry data
16
+ - [x] without selected account
17
+ - [x] infer time-account from ticket-format
18
+ - [x] support user-supplied ticket-parsers
19
+ - [x] get ticket-parser from tags
20
+ - [ ] merge equal adjacent entries into one
21
+ - [ ] complete login/entry automation
22
+ - [ ] handle authentication
23
+ - [ ] login and store cookie
24
+ - [ ] send user and pwd with every request
25
+ - [ ] make entries
26
+ - [ ] open day in browser for review
27
+ - [ ] avoid duplicate entries
28
+ - [ ] start/end time as indicator?
29
+ - [x] offer rounding times to the next 5, 10 or 15 minutes
30
+ - [ ] allow to add entries from the command-line
31
+ - [ ] handle time-account and billable better
32
+ - [ ] import time-accounts from ptime (https://time.puzzle.ch/work_items/search.json?q=search%20terms)
33
+ - [ ] with a dedicated cli?
34
+ - [ ] from a REST-Endpoint of PTime?
35
+ - [ ] automatically prefill billable
36
+ - [x] from time-accounts
37
+ - [ ] from *-notation
38
+ - [ ] allow to have a list of "favourite" time-accounts
39
+ - [ ] select best-matching time-account according to tags, possibly limited to the favourites
40
+ - [ ] add cli-help
41
+ - [ ] use commander for CLI?
42
+
43
+ ## Installation
44
+
45
+ Install it with:
46
+
47
+ $ gem install ptimelog
48
+
49
+ ## Usage
50
+
51
+ $ ptimelog ACTION DATE
52
+
53
+ ### Actions
54
+
55
+ Currently supported actions are
56
+
57
+ - show
58
+ - upload
59
+ - edit
60
+
61
+ ### Date-Identifier
62
+
63
+ To handle a specific date, the format YYYY-MM-DD is expected, e.g. 2017-12-25. Please note that you should not work on that day, unless you bring presents.
64
+
65
+ For reusability in a shell-history the following keywords are supported:
66
+
67
+ - today
68
+ - yesterday
69
+ - last
70
+ - all
71
+
72
+ If nothing is specified, the action is applied to entries of the last day.
73
+
74
+ ### Edit-Identifier
75
+
76
+ When the action is "edit", the next argument is treated as script that should be edited.
77
+
78
+ If nothing is passed, the main timelog.txt is loaded.
79
+
80
+ Otherwise, a script to determine the time-account is loaded.
81
+
82
+ ## Helper-Scripts
83
+
84
+ ptimelog can prefill the account-number and billable-state of an entry.
85
+
86
+ The tags are used to determine a script that helps infer the time-account.
87
+ These scripts should be located in `~/.config/ptimelog/parsers/` and be named
88
+ like the first tag used. The script gets the ticket, the description and all
89
+ remaining tags passed as arguments. The output of the script should only be the
90
+ ID of the time-account.
91
+
92
+ In order to infer the billable-state of an entry, a script
93
+ `~/.config/ptimelog/billable` is called. It only gets the previously infered
94
+ account-id as argument and is expected to output "true" or "false".
95
+
96
+ Since these script are called a lot, it is better to write them in a compiled
97
+ language. If you only like ruby, take a look at crystal. For such simple
98
+ scripts, the code is almost identical and "just" needs to be compiled.
99
+
100
+ ## Configuration
101
+
102
+ A config-file is read from `$HOME/.config/ptimelog/config`. It is expected
103
+ to be a YAML-file. Currently, it supports the following keys:
104
+
105
+ - rounding: [integer or false, default 15]
106
+ - base_url: [url to your puzzletime-installation, default https://time.puzzle.ch]
107
+
108
+ ## Development
109
+
110
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
111
+ `rake spec` to run the tests. You can also run `bin/console` for an interactive
112
+ prompt that will allow you to experiment. Run `bundle exec ptimelog` to use
113
+ the gem in this directory, ignoring other installed copies of this gem.
114
+
115
+ To install this gem onto your local machine, run `bundle exec rake install`. To
116
+ release a new version, update the version number in `version.rb`, and then run
117
+ `bundle exec rake release`, which will create a git tag for the version, push
118
+ git commits and tags, and push the `.gem` file to
119
+ [rubygems.org](https://rubygems.org).
120
+
121
+ ## Contributing
122
+
123
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kronn/ptimelog.
124
+
125
+
126
+ ## License
127
+
128
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/clean'
4
+ CLOBBER.include 'pkg'
5
+
6
+ require 'bundler/gem_helper'
7
+ Bundler::GemHelper.install_tasks name: 'ptimelog'
8
+
9
+ require 'rspec/core/rake_task'
10
+ RSpec::Core::RakeTask.new(:spec)
11
+
12
+ require 'rubocop/rake_task'
13
+ RuboCop::RakeTask.new
14
+
15
+ task default: %i[rubocop spec]
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'ptimelog'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/gpuzzletime ADDED
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'pathname'
6
+
7
+ def upgrade_needed?
8
+ Pathname.new('~/.config/gpuzzletime').expand_path.exist?
9
+ end
10
+
11
+ def upgrade_config
12
+ FileUtils.mv Pathname.new('~/.config/gpuzzletime').expand_path,
13
+ Pathname.new('~/.config/ptimelog').expand_path,
14
+ verbose: true
15
+ end
16
+
17
+ STDERR.puts <<~MESSAGE
18
+
19
+ =========================================================================
20
+
21
+ The name of the project has changed. Please use ptimelog from now on.
22
+
23
+ =========================================================================
24
+
25
+ MESSAGE
26
+
27
+ if upgrade_needed?
28
+ STDERR.puts <<~MESSAGE
29
+ This script can automatically rename the directory for
30
+ configuration and external scripts on your machine.
31
+
32
+ MESSAGE
33
+
34
+ puts 'Upgrade now? [Yn]'
35
+ answer = STDIN.gets.chomp
36
+
37
+ if answer.empty? || answer =~ /^y/i
38
+ upgrade_config
39
+ puts 'Done.'
40
+ else
41
+ puts 'Okay, leaving everything as it were.'
42
+ end
43
+ puts
44
+ end
45
+
46
+ STDERR.puts <<~MESSAGE
47
+ =========================================================================
48
+
49
+ No further action has been taken.
50
+
51
+ =========================================================================
52
+
53
+ MESSAGE
data/exe/ptimelog ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/ptimelog'
5
+
6
+ Ptimelog::App.new(ARGV).run
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'gpuzzletime'
5
+ spec.version = '0.5.0'
6
+ spec.authors = ['Matthias Viehweger']
7
+ spec.email = ['kronn@kronn.de']
8
+
9
+ spec.summary = 'The gem formerly known as gpuzzletime is now ptimelog'
10
+ spec.homepage = 'https://github.com/kronn/ptimelog'
11
+ spec.license = 'MIT'
12
+
13
+ spec.add_runtime_dependency 'ptimelog'
14
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ # Wrapper for everything
5
+ class App
6
+ def initialize(args)
7
+ @config = Configuration.instance
8
+ command = (args[0] || :show).to_sym
9
+
10
+ @command = case command
11
+ when :show
12
+ @date = NamedDate.new.date(args[1])
13
+ Command::Show.new
14
+ when :upload
15
+ @date = NamedDate.new.date(args[1])
16
+ Command::Upload.new
17
+ when :edit
18
+ file = args[1]
19
+ Command::Edit.new(file)
20
+ else
21
+ raise ArgumentError, "Unsupported Command #{@command}"
22
+ end
23
+ end
24
+
25
+ def run
26
+ @command.entries = entries if @command.needs_entries?
27
+
28
+ @command.run
29
+ end
30
+
31
+ private
32
+
33
+ def timelog
34
+ Timelog.load
35
+ end
36
+
37
+ def entries
38
+ timelog.each_with_object({}) do |(date, lines), entries|
39
+ next unless date # guard against the machine
40
+ next unless @date == :all || @date == date # limit to one day if passed
41
+
42
+ entries[date] = []
43
+ start = nil # at the start of the day, we have no previous end
44
+
45
+ lines.each do |line|
46
+ entry = Entry.from_timelog(line)
47
+ entry.start_time = start
48
+
49
+ entries[date] << entry if entry.valid?
50
+
51
+ start = entry.finish_time # store previous ending for nice display of next entry
52
+ end
53
+
54
+ entries
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ module Command
5
+ # Foundation and common API for all commands
6
+ class Base
7
+ def initialize
8
+ @config = Configuration.instance
9
+ @entries = {} if needs_entries?
10
+ end
11
+
12
+ def needs_entries?
13
+ false
14
+ end
15
+
16
+ def run
17
+ raise 'Implement a run-method for your command'
18
+ end
19
+
20
+ def entries=(_values)
21
+ raise 'Implement a entries-writer-method for your command' if needs_entries?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ module Command
5
+ # edit one file. without argument, it will edit the timelog, otherwise a
6
+ # parser-script is loaded
7
+ class Edit < Base
8
+ def initialize(file)
9
+ super()
10
+
11
+ @scripts = Script.new(@config[:dir])
12
+ @file = file
13
+ end
14
+
15
+ def run
16
+ launch_editor(@file)
17
+ end
18
+
19
+ private
20
+
21
+ def launch_editor(file)
22
+ editor = `which $EDITOR`.chomp
23
+
24
+ file = file.nil? ? Timelog.timelog_txt : @scripts.parser(@file)
25
+
26
+ exec "#{editor} #{file}"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ module Command
5
+ # show entries of one day or all of them
6
+ class Show < Base
7
+ def needs_entries?
8
+ true
9
+ end
10
+
11
+ def run
12
+ @entries.each do |date, list|
13
+ puts date, '----------'
14
+ list.each do |entry|
15
+ puts entry
16
+ end
17
+ puts nil
18
+ end
19
+ end
20
+
21
+ def entries=(entries)
22
+ entries.each do |date, list|
23
+ @entries[date] = []
24
+
25
+ list.each do |entry|
26
+ @entries[date] << [
27
+ entry.start_time, '-', entry.finish_time,
28
+ [
29
+ entry.ticket,
30
+ entry.description,
31
+ entry.tags,
32
+ entry.account,
33
+ ].compact.join(' ∴ '),
34
+ ].compact.join(' ')
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Ptimelog
6
+ module Command
7
+ # Upload entries to puzzletime
8
+ class Upload < Base
9
+ attr_writer :entries
10
+
11
+ def needs_entries?
12
+ true
13
+ end
14
+
15
+ def run
16
+ @entries.each do |date, list|
17
+ puts "Uploading #{date}"
18
+ list.each do |entry|
19
+ open_browser(entry)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def open_browser(entry)
27
+ xdg_open "'#{@config[:base_url]}/ordertimes/new?#{url_options(entry)}'", silent: true
28
+ end
29
+
30
+ def xdg_open(args, silent: false)
31
+ opener = 'xdg-open' # could be configurable, but is already a proxy
32
+ silencer = '> /dev/null 2> /dev/null'
33
+
34
+ if system("which #{opener} #{silencer}")
35
+ system "#{opener} #{args} #{silencer if silent}"
36
+ else
37
+ abort <<~ERRORMESSAGE
38
+ #{opener} not found
39
+
40
+ This binary is needed to launch a webbrowser and open the page
41
+ to enter the worktime-entry into puzzletime.
42
+
43
+ If this needs to be configurable, please open an issue at
44
+ https://github.com/kronn/ptimelog/issues/new
45
+ ERRORMESSAGE
46
+ end
47
+ end
48
+
49
+ def url_options(entry)
50
+ {
51
+ work_date: entry.date,
52
+ 'ordertime[ticket]': entry.ticket,
53
+ 'ordertime[description]': entry.description,
54
+ 'ordertime[from_start_time]': entry.start_time,
55
+ 'ordertime[to_end_time]': entry.finish_time,
56
+ 'ordertime[account_id]': entry.account,
57
+ 'ordertime[billable]': entry.billable,
58
+ }
59
+ .map { |key, value| [key, ERB::Util.url_encode(value)].join('=') }
60
+ .join('&')
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'pathname'
5
+
6
+ module Ptimelog
7
+ # Wrapper around configuration-options and -loading
8
+ class Configuration
9
+ include Singleton
10
+
11
+ CONFIGURATION_DEFAULTS = {
12
+ base_url: 'https://time.puzzle.ch',
13
+ rounding: 15,
14
+ dir: '~/.config/ptimelog',
15
+ timelog: '~/.local/share/gtimelog/timelog.txt',
16
+ }.freeze
17
+
18
+ def initialize
19
+ reset
20
+ end
21
+
22
+ def reset
23
+ @config = load_config(
24
+ Pathname.new(CONFIGURATION_DEFAULTS[:dir]).join('config')
25
+ )
26
+ wrap_with_pathname(:dir)
27
+ wrap_with_pathname(:timelog)
28
+ end
29
+
30
+ def load_config(fn)
31
+ user_config = fn.exist? ? YAML.load_file(fn) : {}
32
+
33
+ CONFIGURATION_DEFAULTS.merge(user_config)
34
+ end
35
+
36
+ def [](key)
37
+ @config[key.to_sym]
38
+ end
39
+
40
+ def []=(key, value)
41
+ @config[key.to_sym] = value
42
+
43
+ wrap_with_pathname(key.to_sym) if %w[dir timelog].include?(key.to_s)
44
+ end
45
+
46
+ private
47
+
48
+ def wrap_with_pathname(key)
49
+ return unless @config.key?(key)
50
+ return @config[key] if @config[key].is_a? Pathname
51
+
52
+ @config[key] = Pathname.new(@config[key]).expand_path
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ # Dataclass to wrap an entry
5
+ class Entry
6
+ # allow to read everything
7
+ attr_reader :date, :start_time, :finish_time, :ticket, :description,
8
+ :tags, :billable, :account
9
+
10
+ # define only trivial writers, omit special and derived values
11
+ attr_writer :date, :ticket, :description
12
+
13
+ def initialize(config = Configuration.instance)
14
+ @config = config
15
+ @script = Script.new(@config[:dir])
16
+ end
17
+
18
+ class << self
19
+ def from_timelog(matched_line)
20
+ entry = new
21
+ entry.from_timelog(matched_line)
22
+ entry
23
+ end
24
+ end
25
+
26
+ def from_timelog(matched_line)
27
+ self.date = matched_line[:date]
28
+ self.ticket = matched_line[:ticket]
29
+ self.description = matched_line[:description]
30
+ self.finish_time = matched_line[:time]
31
+ self.tags = matched_line[:tags]
32
+
33
+ infer_ptime_settings
34
+ end
35
+
36
+ def start_time=(time)
37
+ @start_time = round_time(time, @config[:rounding])
38
+ end
39
+
40
+ def finish_time=(time)
41
+ @finish_time = round_time(time, @config[:rounding])
42
+ end
43
+
44
+ def tags=(tags)
45
+ @tags = case tags
46
+ when '', nil then nil
47
+ else tags.split.compact
48
+ end
49
+ end
50
+
51
+ def valid?
52
+ @start_time && !hidden?
53
+ end
54
+
55
+ def hidden?
56
+ @description =~ /\*\*$/ # hide lunch and breaks
57
+ end
58
+
59
+ def infer_ptime_settings
60
+ @account = infer_account
61
+ @billable = infer_billable
62
+ end
63
+
64
+ def to_s
65
+ [
66
+ @start_time, '-', @finish_time,
67
+ [
68
+ @ticket,
69
+ @description,
70
+ @tags,
71
+ @account,
72
+ ].compact.join(' : '),
73
+ ].compact.join(' ')
74
+ end
75
+
76
+ # make sortable/def <=>
77
+ # duration if start and finish is set
78
+
79
+ private
80
+
81
+ def round_time(time, interval)
82
+ return time unless interval
83
+ return unless time.to_s =~ /\d\d:\d\d/
84
+
85
+ hour, minute = time.split(':')
86
+ minute = (minute.to_i / interval.to_f).round * interval.to_i
87
+
88
+ if minute == 60
89
+ [hour.succ, 0]
90
+ else
91
+ [hour, minute]
92
+ end.map { |part| part.to_s.rjust(2, '0') }.join(':')
93
+ end
94
+
95
+ def infer_account
96
+ return unless @tags
97
+
98
+ parser_name = @tags.first
99
+ parser = @script.parser(parser_name)
100
+
101
+ return unless parser.exist?
102
+
103
+ cmd = %(#{parser} "#{@ticket}" "#{@description}" #{@tags[1..-1].map(&:inspect).join(' ')})
104
+ `#{cmd}`.chomp # maybe only execute if parser is in correct dir?
105
+ end
106
+
107
+ def infer_billable
108
+ script = @script.billable
109
+
110
+ return 1 unless script.exist?
111
+
112
+ `#{script} #{@account}`.chomp == 'true' ? 1 : 0
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Ptimelog
6
+ # Mapping between semantic/relative names and absolute dates
7
+ class NamedDate
8
+ def date(arg = 'last')
9
+ named_date(arg) || :all
10
+ end
11
+
12
+ def named_date(date)
13
+ case date.to_s
14
+ when 'yesterday' then Date.today.prev_day.to_s
15
+ when 'today' then Date.today.to_s
16
+ when 'last', '' then timelog.to_h.keys.compact.sort[-2] || Date.today.prev_day.to_s
17
+ when /\d{4}(-\d{2}){2}/ then date
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def timelog
24
+ Timelog.load
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ # Wrapper around all external scripts that might be called to get more
5
+ # information about the time-entries
6
+ class Script
7
+ def initialize(config_dir)
8
+ @config_dir = config_dir
9
+ end
10
+
11
+ def parser(parser_name)
12
+ @config_dir.join("parsers/#{parser_name}") # FIXME: security-hole, prevent relative paths!
13
+ .expand_path
14
+ end
15
+
16
+ def billable
17
+ @config_dir.join('billable').expand_path
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Ptimelog
6
+ # Load and tokenize the data from gtimelog
7
+ class Timelog
8
+ include Singleton
9
+
10
+ class << self
11
+ def load
12
+ instance.load
13
+ end
14
+
15
+ def timelog_txt
16
+ Pathname.new(Configuration.instance[:timelog]).expand_path
17
+ end
18
+ end
19
+
20
+ def load
21
+ @load ||= parse(read)
22
+ end
23
+
24
+ def timelog_txt
25
+ self.class.timelog_txt
26
+ end
27
+
28
+ def read
29
+ timelog_txt.read
30
+ end
31
+
32
+ def parse(data)
33
+ data.split("\n")
34
+ .map { |line| tokenize(line) }
35
+ .group_by { |match| match && match[:date] }
36
+ .to_a
37
+ end
38
+
39
+ def tokenize(line)
40
+ re_date = /(?<date>\d{4}-\d{2}-\d{2})/
41
+ re_time = /(?<time>\d{2}:\d{2})/
42
+ re_tick = /(?:(?<ticket>.*?): )/
43
+ re_desc = /(?<description>.*?)/
44
+ re_tags = /(?: -- (?<tags>.*)?)/
45
+
46
+ regexp = /^#{re_date} #{re_time}: #{re_tick}?#{re_desc}#{re_tags}?$/
47
+ line.match(regexp)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Version
4
+ module Ptimelog
5
+ VERSION = '0.5.0'
6
+ end
data/lib/ptimelog.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__)
4
+
5
+ # Autoloading and such
6
+ module Ptimelog
7
+ autoload :App, 'ptimelog/app'
8
+ autoload :Configuration, 'ptimelog/configuration'
9
+ autoload :Entry, 'ptimelog/entry'
10
+ autoload :NamedDate, 'ptimelog/named_date'
11
+ autoload :Script, 'ptimelog/script'
12
+ autoload :Timelog, 'ptimelog/timelog'
13
+ autoload :VERSION, 'ptimelog/version'
14
+
15
+ # Collection of commands available at the CLI
16
+ module Command
17
+ autoload :Base, 'ptimelog/command/base'
18
+ autoload :Edit, 'ptimelog/command/edit'
19
+ autoload :Show, 'ptimelog/command/show'
20
+ autoload :Upload, 'ptimelog/command/upload'
21
+ end
22
+ end
data/ptimelog.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'ptimelog/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'ptimelog'
9
+ spec.version = Ptimelog::VERSION
10
+ spec.authors = ['Matthias Viehweger']
11
+ spec.email = ['kronn@kronn.de']
12
+
13
+ spec.summary = 'Move time-entries from gTimelog to PuzzleTime'
14
+ # spec.description = %q{}
15
+ spec.homepage = 'https://github.com/kronn/ptimelog'
16
+ spec.license = 'MIT'
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler'
24
+ spec.add_development_dependency 'overcommit', '~> 0.45'
25
+ spec.add_development_dependency 'pry', '~> 0.12'
26
+ spec.add_development_dependency 'rake', '~> 10.0'
27
+ spec.add_development_dependency 'rspec', '~> 3.0'
28
+ spec.add_development_dependency 'rubocop', '~> 0.50'
29
+ spec.add_development_dependency 'timecop', '~> 0.9'
30
+ end
metadata ADDED
@@ -0,0 +1,173 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ptimelog
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthias Viehweger
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-08-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: overcommit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.45'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.45'
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.12'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.12'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '3.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '3.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.50'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.50'
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ description:
112
+ email:
113
+ - kronn@kronn.de
114
+ executables:
115
+ - gpuzzletime
116
+ - ptimelog
117
+ extensions: []
118
+ extra_rdoc_files: []
119
+ files:
120
+ - ".gitignore"
121
+ - ".overcommit.yml"
122
+ - ".rspec"
123
+ - ".rubocop.yml"
124
+ - ".rubocop_todo.yml"
125
+ - ".ruby-gemset"
126
+ - ".ruby-version"
127
+ - ".travis.yml"
128
+ - Gemfile
129
+ - LICENSE.txt
130
+ - README.md
131
+ - Rakefile
132
+ - bin/console
133
+ - bin/setup
134
+ - exe/gpuzzletime
135
+ - exe/ptimelog
136
+ - gpuzzletime.gemspec
137
+ - lib/ptimelog.rb
138
+ - lib/ptimelog/app.rb
139
+ - lib/ptimelog/command/base.rb
140
+ - lib/ptimelog/command/edit.rb
141
+ - lib/ptimelog/command/show.rb
142
+ - lib/ptimelog/command/upload.rb
143
+ - lib/ptimelog/configuration.rb
144
+ - lib/ptimelog/entry.rb
145
+ - lib/ptimelog/named_date.rb
146
+ - lib/ptimelog/script.rb
147
+ - lib/ptimelog/timelog.rb
148
+ - lib/ptimelog/version.rb
149
+ - ptimelog.gemspec
150
+ homepage: https://github.com/kronn/ptimelog
151
+ licenses:
152
+ - MIT
153
+ metadata: {}
154
+ post_install_message:
155
+ rdoc_options: []
156
+ require_paths:
157
+ - lib
158
+ required_ruby_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ required_rubygems_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ requirements: []
169
+ rubygems_version: 3.0.3
170
+ signing_key:
171
+ specification_version: 4
172
+ summary: Move time-entries from gTimelog to PuzzleTime
173
+ test_files: []