ptimelog 0.5.3 → 0.10.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4683decb77f362d2861b903e30db9dcb330151f7ee43d6ddb3c332d3af4243b2
4
- data.tar.gz: 23c0c132975613fb3f16c064930e6aa72b49e28fbd7cd9b3636aef6863cbb1e4
3
+ metadata.gz: 3a7ad17dbf09bc8d7694f9261ffcc8216af2f1ec04be03e62ccabadf9732e8b0
4
+ data.tar.gz: 4ee34a775b9300e50971421adedee472fbfa5f5c0ec226b93e299d17474df411
5
5
  SHA512:
6
- metadata.gz: e24c93aefa5a8ae475013593af737f872bade8f0f02997551bd2fe2cce049e64bd42869fe9648630f1b02191f2f7fc87883b942a9e266492c0fa09227c51b592
7
- data.tar.gz: 043b2383d127e1aeccb716f8e14e47a365cdc309862c0d1309916ceb1695b194e1cb9aad1e4bc5f63f3b9a95aa3aae55981606018370b03d077a1f5a3d42159a
6
+ metadata.gz: 198b9c6aefbab6f64b617a6b2ad125f9e9921c829e1740ca08faa41c69c797fda1cca1181665adec6b68b51560b3a5bed88ad3b23a8bceba6382dd4b222bb129
7
+ data.tar.gz: d37ed829daa68c7527caafe929874c6523986609661f9a9155463f0307ac2fb9d14d3237878d6c6659f799ca59c9536e8cae46770f21cdb13916753277beaf2c
data/.envrc ADDED
@@ -0,0 +1 @@
1
+ PATH_add exe
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /spec/state.txt
@@ -1,6 +1,15 @@
1
1
  inherit_from: .rubocop_todo.yml
2
2
 
3
- Metrics/LineLength:
3
+ # require:
4
+ # - rubocop-rake
5
+ # - rubocop-rspec
6
+ # - rubocop-packaging
7
+ # - rubocop-performance
8
+
9
+ AllCops:
10
+ NewCops: enable
11
+
12
+ Layout/LineLength:
4
13
  Max: 120
5
14
 
6
15
  Metrics/BlockLength:
@@ -13,12 +22,12 @@ Style/TrailingCommaInArrayLiteral:
13
22
  Style/TrailingCommaInHashLiteral:
14
23
  EnforcedStyleForMultiline: consistent_comma
15
24
 
16
- Layout/AlignHash:
25
+ Layout/HashAlignment:
17
26
  EnforcedHashRocketStyle: table
18
27
  EnforcedColonStyle: table
19
28
  EnforcedLastArgumentHashStyle: always_inspect # default
20
29
 
21
- Naming/UncommunicativeMethodParamName:
30
+ Naming/MethodParameterName:
22
31
  AllowedNames:
23
32
  - fn
24
33
  # default allowed names
@@ -1,12 +1,12 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2019-08-06 22:23:40 +0200 using RuboCop version 0.65.0.
3
+ # on 2020-01-22 14:10:50 +0100 using RuboCop version 0.79.0.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
- # Offense count: 3
9
+ # Offense count: 4
10
10
  Metrics/AbcSize:
11
11
  Max: 16
12
12
 
@@ -1 +1 @@
1
- 2.6.3
1
+ 2.7.0
@@ -0,0 +1 @@
1
+ ruby 2.7.2
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2017-2018 Matthias Viehweger
3
+ Copyright (c) 2017-2020 Matthias Viehweger
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # pTimeLog
2
2
 
3
- small tooling to transfer timelog-entries from gtimelog's timelog.txt to the PuzzleTime TimeTracking Website.
3
+ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the PuzzleTime Timetracking Website.
4
+
5
+ [![Build Status](https://travis-ci.org/kronn/ptimelog.svg?branch=master)](https://travis-ci.org/kronn/ptimelog)
4
6
 
5
7
  ## Approach
6
8
 
@@ -17,17 +19,17 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
17
19
  - [x] infer time-account from ticket-format
18
20
  - [x] support user-supplied ticket-parsers
19
21
  - [x] get ticket-parser from tags
20
- - [ ] merge equal adjacent entries into one
22
+ - [x] merge equal adjacent entries into one
21
23
  - [ ] complete login/entry automation
22
24
  - [ ] handle authentication
23
- - [ ] login and store cookie
25
+ - [ ] login and store cookie (https://stackoverflow.com/questions/12399087/curl-to-access-a-page-that-requires-a-login-from-a-different-page#12399176)
24
26
  - [ ] send user and pwd with every request
25
27
  - [ ] make entries
26
28
  - [ ] open day in browser for review
27
- - [ ] avoid duplicate entries
28
- - [ ] start/end time as indicator?
29
+ - [x] avoid duplicate entries
30
+ - [x] start/end time as indicator?
29
31
  - [x] offer rounding times to the next 5, 10 or 15 minutes
30
- - [ ] allow to add entries from the command-line
32
+ - [x] allow to add entries from the command-line
31
33
  - [ ] handle time-account and billable better
32
34
  - [ ] import time-accounts from ptime (https://time.puzzle.ch/work_items/search.json?q=search%20terms)
33
35
  - [ ] with a dedicated cli?
@@ -37,6 +39,7 @@ small tooling to transfer timelog-entries from gtimelog's timelog.txt to the Puz
37
39
  - [ ] from *-notation
38
40
  - [ ] allow to have a list of "favourite" time-accounts
39
41
  - [ ] select best-matching time-account according to tags, possibly limited to the favourites
42
+ - [x] combine billable and account-lookup into one script
40
43
  - [ ] add cli-help
41
44
  - [ ] use commander for CLI?
42
45
 
@@ -57,10 +60,13 @@ Currently supported actions are
57
60
  - show
58
61
  - upload
59
62
  - edit
63
+ - add
64
+ - version
60
65
 
61
66
  ### Date-Identifier
62
67
 
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.
68
+ To handle a specific date, the format YYYY-MM-DD is expected, e.g. 2017-12-25.
69
+ Please note that you should not work on that day, unless you bring presents.
64
70
 
65
71
  For reusability in a shell-history the following keywords are supported:
66
72
 
@@ -73,27 +79,58 @@ If nothing is specified, the action is applied to entries of the last day.
73
79
 
74
80
  ### Edit-Identifier
75
81
 
76
- When the action is "edit", the next argument is treated as script that should be edited.
82
+ When the action is "edit", the next argument is treated as script that should
83
+ be edited.
77
84
 
78
85
  If nothing is passed, the main timelog.txt is loaded.
79
86
 
80
87
  Otherwise, a script to determine the time-account is loaded.
81
88
 
89
+ ### Adding entries
90
+
91
+ In order to add entries with the ptimelog-cli, the complete entry needs to be
92
+ quoted on the command-line to count as one argument.
93
+
94
+ $ ptimelog add 'ticket 1337: Implement requirements -- client coding'
95
+
96
+ While this requires some knowledge of the file-format, it is no different than
97
+ entering the same string in gTimelog. For now, the entry is added to the
98
+ timelog.txt as it is passed. By default, the date/time added to the entry is
99
+ the one when the command is executed.
100
+
101
+ You can prefix a positive or negative signed number to slightly skew the entry
102
+ (think: '-5 meeting' or '+5 lunch \*\*') or even set a precise time ('10:30
103
+ meeting').
104
+
105
+ $ ptimelog add '-5 meeting: Discuss requirements -- client planning'
106
+
107
+ ### Showing the Version
108
+
109
+ I got tired of asking rubygems which version I installed, so I took on the
110
+ herculean task of letting ptimelog show its own version.
111
+
112
+ ### Formatting the Output
113
+
114
+ In order to format the output of the show-action into a table, a hopefully
115
+ convienient field-marker has been chosen. I think it is unlikely, that ∴ is
116
+ being used in a time-entry. Therefore, you can pipe the output into `column`:
117
+
118
+ ptimelog show today | column -t -s ∴
119
+
82
120
  ## Helper-Scripts
83
121
 
84
122
  ptimelog can prefill the account-number and billable-state of an entry.
85
123
 
86
124
  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
125
+ These scripts should be located in `~/.config/ptimelog/inferers/` and be named
88
126
  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.
127
+ remaining tags passed as arguments.
91
128
 
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".
129
+ The output of the script should be the ID of the time-account and the
130
+ billable-state as "true" or "false". Both items need to be separated by
131
+ whitespace, so you can output those two on the same line or on different lines.
95
132
 
96
- Since these script are called a lot, it is better to write them in a compiled
133
+ Since these scripts are called a lot, it is better to write them in a compiled
97
134
  language. If you only like ruby, take a look at crystal. For such simple
98
135
  scripts, the code is almost identical and "just" needs to be compiled.
99
136
 
@@ -102,8 +139,8 @@ scripts, the code is almost identical and "just" needs to be compiled.
102
139
  A config-file is read from `$HOME/.config/ptimelog/config`. It is expected
103
140
  to be a YAML-file. Currently, it supports the following keys:
104
141
 
105
- - rounding: [integer or false, default 15]
106
- - base_url: [url to your puzzletime-installation, default https://time.puzzle.ch]
142
+ - rounding: [integer or false, default 15]
143
+ - base_url: [url to your puzzletime-installation, default https://time.puzzle.ch]
107
144
 
108
145
  ## Development
109
146
 
@@ -6,17 +6,22 @@ $LOAD_PATH.unshift File.dirname(__FILE__)
6
6
  module Ptimelog
7
7
  autoload :App, 'ptimelog/app'
8
8
  autoload :Configuration, 'ptimelog/configuration'
9
+ autoload :Day, 'ptimelog/day'
10
+ autoload :DeprecationWarning, 'ptimelog/deprecation_warning'
9
11
  autoload :Entry, 'ptimelog/entry'
10
12
  autoload :NamedDate, 'ptimelog/named_date'
13
+ autoload :NullPathname, 'ptimelog/null_pathname'
11
14
  autoload :Script, 'ptimelog/script'
12
15
  autoload :Timelog, 'ptimelog/timelog'
13
16
  autoload :VERSION, 'ptimelog/version'
14
17
 
15
18
  # Collection of commands available at the CLI
16
19
  module Command
20
+ autoload :Add, 'ptimelog/command/add'
17
21
  autoload :Base, 'ptimelog/command/base'
18
22
  autoload :Edit, 'ptimelog/command/edit'
19
23
  autoload :Show, 'ptimelog/command/show'
20
24
  autoload :Upload, 'ptimelog/command/upload'
25
+ autoload :Version, 'ptimelog/command/version'
21
26
  end
22
27
  end
@@ -1,58 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Ptimelog
4
- # Wrapper for everything
4
+ # Wrapper for everything, dispatching to a command
5
5
  class App
6
6
  def initialize(args)
7
7
  @config = Configuration.instance
8
- command = (args[0] || :show).to_sym
8
+ command = (args[0] || 'show')
9
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
10
+ constant_name = command.to_s[0].upcase + command[1..].downcase
11
+ command_class = Command.const_get(constant_name.to_sym)
12
+ raise ArgumentError, "Unsupported Command '#{command}'" if command_class.nil?
13
+
14
+ @command = command_class.new(args[1]) # e.g. Ptimelog::Command::Show.new('today')
23
15
  end
24
16
 
25
17
  def run
26
- @command.entries = entries if @command.needs_entries?
27
-
28
18
  @command.run
29
19
  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
20
  end
58
21
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Ptimelog
6
+ module Command
7
+ # add a new entrie with the current date and time
8
+ class Add < Base
9
+ def initialize(task)
10
+ super()
11
+
12
+ @task = task
13
+ @timelog = Ptimelog::Timelog.instance
14
+ @new_lines = []
15
+ end
16
+
17
+ def needs_entries?
18
+ false
19
+ end
20
+
21
+ def run
22
+ add_empty_line if @timelog.previous_entry.date == yesterday
23
+ add_entry(*parse_task(@task))
24
+
25
+ save_file
26
+ end
27
+
28
+ private
29
+
30
+ def parse_task(line)
31
+ matches = line.match('(?<time>\d{1,2}:\d{2} )?(?<offset>[+-]\d+ )?(?<task>.*)')
32
+ formatted_time = if matches[:time]
33
+ Time.parse(matches[:time])
34
+ else
35
+ Time.now
36
+ end
37
+ .localtime
38
+ .then { |time| time + (matches[:offset].to_i * 60) }
39
+ .strftime('%F %R')
40
+
41
+ [formatted_time, matches[:task]]
42
+ end
43
+
44
+ def add_entry(date_time, task)
45
+ @new_lines << "#{date_time}: #{task}"
46
+ end
47
+
48
+ def add_empty_line
49
+ @new_lines << ''
50
+ end
51
+
52
+ def save_file
53
+ @timelog.timelog_txt.open('a') do |log|
54
+ @new_lines.each do |line|
55
+ log << "#{line}\n"
56
+ end
57
+ end
58
+ end
59
+
60
+ def yesterday
61
+ NamedDate.new.named_date('yesterday')
62
+ end
63
+ end
64
+ end
65
+ end
@@ -4,9 +4,13 @@ module Ptimelog
4
4
  module Command
5
5
  # Foundation and common API for all commands
6
6
  class Base
7
- def initialize
8
- @config = Configuration.instance
9
- @entries = {} if needs_entries?
7
+ def initialize(day = nil)
8
+ @config = Configuration.instance
9
+
10
+ return unless needs_entries?
11
+
12
+ @entries = {}
13
+ self.entries = Ptimelog::Day.new(day).entries
10
14
  end
11
15
 
12
16
  def needs_entries?
@@ -13,7 +13,7 @@ module Ptimelog
13
13
  end
14
14
 
15
15
  def run
16
- launch_editor(@file)
16
+ launch_editor(find_file(@file))
17
17
  end
18
18
 
19
19
  private
@@ -21,10 +21,34 @@ module Ptimelog
21
21
  def launch_editor(file)
22
22
  editor = `which $EDITOR`.chomp
23
23
 
24
- file = file.nil? ? Timelog.timelog_txt : @scripts.parser(@file)
25
-
26
24
  exec "#{editor} #{file}"
27
25
  end
26
+
27
+ def find_file(requested_file)
28
+ %i[
29
+ timelog
30
+ existing_inferer
31
+ empty_inferer
32
+ ].each do |file_lookup|
33
+ valid, filename = send(file_lookup, requested_file)
34
+
35
+ return filename if valid
36
+ end
37
+ end
38
+
39
+ def timelog(file)
40
+ [file.nil?, Timelog.timelog_txt]
41
+ end
42
+
43
+ def existing_inferer(file)
44
+ fn = @scripts.inferer(file)
45
+
46
+ [fn.exist?, fn]
47
+ end
48
+
49
+ def empty_inferer(file)
50
+ [true, @scripts.inferer(file)]
51
+ end
28
52
  end
29
53
  end
30
54
  end
@@ -4,17 +4,30 @@ module Ptimelog
4
4
  module Command
5
5
  # show entries of one day or all of them
6
6
  class Show < Base
7
+ def initialize(*args)
8
+ @durations = Hash.new(0)
9
+
10
+ super
11
+ end
12
+
7
13
  def needs_entries?
8
14
  true
9
15
  end
10
16
 
11
17
  def run
12
18
  @entries.each do |date, list|
13
- puts date, '----------'
19
+ puts date,
20
+ '----------'
21
+
22
+ next if list.empty?
23
+
14
24
  list.each do |entry|
15
25
  puts entry
16
26
  end
17
- puts nil
27
+ puts '----------',
28
+ "Total work done: #{duration(date)} hours",
29
+ '----------------------------',
30
+ nil
18
31
  end
19
32
  end
20
33
 
@@ -23,18 +36,15 @@ module Ptimelog
23
36
  @entries[date] = []
24
37
 
25
38
  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(' ')
39
+ @durations[date] += entry.duration
40
+ @entries[date] << entry.to_s
35
41
  end
36
42
  end
37
43
  end
44
+
45
+ def duration(date)
46
+ Time.at(@durations[date]).utc.strftime('%H:%M')
47
+ end
38
48
  end
39
49
  end
40
50
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ module Command
5
+ # Output the Version-Number
6
+ class Version < Base
7
+ def initialize(_arg)
8
+ super(nil)
9
+ end
10
+
11
+ def run
12
+ puts Ptimelog::VERSION
13
+ end
14
+ end
15
+ end
16
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'singleton'
4
4
  require 'pathname'
5
+ require 'yaml'
5
6
 
6
7
  module Ptimelog
7
8
  # Wrapper around configuration-options and -loading
@@ -9,10 +10,10 @@ module Ptimelog
9
10
  include Singleton
10
11
 
11
12
  CONFIGURATION_DEFAULTS = {
12
- base_url: 'https://time.puzzle.ch',
13
- rounding: 15,
14
- dir: '~/.config/ptimelog',
15
- timelog: '~/.local/share/gtimelog/timelog.txt',
13
+ 'base_url' => 'https://time.puzzle.ch',
14
+ 'rounding' => 15,
15
+ 'dir' => '~/.config/ptimelog',
16
+ 'timelog' => '~/.local/share/gtimelog/timelog.txt',
16
17
  }.freeze
17
18
 
18
19
  def initialize
@@ -21,10 +22,12 @@ module Ptimelog
21
22
 
22
23
  def reset
23
24
  @config = load_config(
24
- Pathname.new(CONFIGURATION_DEFAULTS[:dir]).join('config')
25
+ Pathname.new(CONFIGURATION_DEFAULTS['dir'])
26
+ .expand_path
27
+ .join('config')
25
28
  )
26
- wrap_with_pathname(:dir)
27
- wrap_with_pathname(:timelog)
29
+ wrap_with_pathname('dir')
30
+ wrap_with_pathname('timelog')
28
31
  end
29
32
 
30
33
  def load_config(fn)
@@ -34,13 +37,13 @@ module Ptimelog
34
37
  end
35
38
 
36
39
  def [](key)
37
- @config[key.to_sym]
40
+ @config[key.to_s]
38
41
  end
39
42
 
40
43
  def []=(key, value)
41
- @config[key.to_sym] = value
44
+ @config[key.to_s] = value
42
45
 
43
- wrap_with_pathname(key.to_sym) if %w[dir timelog].include?(key.to_s)
46
+ wrap_with_pathname(key.to_s) if %w[dir timelog].include?(key.to_s)
44
47
  end
45
48
 
46
49
  private
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ # Wrap loading of all entries of a day
5
+ class Day
6
+ def initialize(date)
7
+ @date = NamedDate.new.date(date)
8
+ end
9
+
10
+ def entries
11
+ timelog.each_with_object({}) do |(date, lines), entries|
12
+ next unless date # guard against the machine
13
+ next unless @date.to_s == 'all' || @date == date # limit to one day if passed
14
+
15
+ entries[date] = join_similar(entries_of_day(lines)) # lines |> entries_of_day |> join_similar
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def timelog
22
+ Timelog.load
23
+ end
24
+
25
+ def entries_of_day(lines)
26
+ entries = []
27
+ start = nil # at the start of the day, we have no previous end
28
+
29
+ lines.each do |line|
30
+ entry = Entry.from_timelog(line)
31
+ entry.start_time = start
32
+
33
+ entries << entry if entry.valid?
34
+
35
+ start = entry.finish_time # store previous ending for nice display of next entry
36
+ end
37
+
38
+ entries
39
+ end
40
+
41
+ def join_similar(list)
42
+ return [] if list.empty?
43
+ return list if list.one?
44
+
45
+ one, *tail = list
46
+ two, *rest = tail
47
+
48
+ joined = maybe_join(one, two)
49
+
50
+ if joined.one?
51
+ join_similar(joined + rest)
52
+ else
53
+ [one] + join_similar(tail)
54
+ end
55
+ end
56
+
57
+ def maybe_join(one, two)
58
+ if one.ticket == two.ticket &&
59
+ one.description == two.description &&
60
+ one.finish_time == two.start_time
61
+
62
+ joined = one.dup
63
+ joined.finish_time = two.finish_time
64
+
65
+ [joined]
66
+ else
67
+ [one, two]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ptimelog
4
+ # Allow to add some (hopefully) helpful deprecation warning
5
+ module DeprecationWarning
6
+ def self.included(base)
7
+ base.send :extend, ClassMethods
8
+ end
9
+
10
+ # Keep track of wether the deprecation have been shown already or not
11
+ module ClassMethods
12
+ def deprecation_warning_rendered?
13
+ @deprecation_warning_rendered == true
14
+ end
15
+
16
+ def reset_deprecation_warning!
17
+ @deprecation_warning_rendered = false
18
+ end
19
+
20
+ def deprecation_warning_rendered!
21
+ @deprecation_warning_rendered = true
22
+ end
23
+ end
24
+
25
+ def deprecate(*args)
26
+ warn deprecate_header(*args)
27
+
28
+ return if self.class.deprecation_warning_rendered?
29
+
30
+ warn deprecate_message(*args)
31
+ self.class.deprecation_warning_rendered!
32
+ end
33
+
34
+ def deprecate_header(_)
35
+ raise <<~MESSAGE
36
+ deprecate_header(args) not implemented
37
+
38
+ Please define a header/short-info for the deprecation, rendered every
39
+ time the deprecation is hit.
40
+ MESSAGE
41
+ end
42
+
43
+ def deprecate_message(_)
44
+ raise <<~MESSAGE
45
+ deprecate_message(args) not implemented
46
+
47
+ Please define a message () for the deprecation, rendered only the first
48
+ time the deprecation is hit.
49
+ MESSAGE
50
+ end
51
+ end
52
+ end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'time'
4
+
3
5
  module Ptimelog
4
6
  # Dataclass to wrap an entry
5
7
  class Entry
6
- # allow to read everything
7
- attr_reader :date, :start_time, :finish_time, :ticket, :description,
8
- :tags, :billable, :account
9
-
10
8
  # define only trivial writers, omit special and derived values
11
- attr_writer :date, :ticket, :description
9
+ attr_accessor :date, :ticket, :description
10
+
11
+ # allow to read everything else
12
+ attr_reader :start_time, :finish_time, :tags, :billable, :account
13
+
14
+ BILLABLE = 1
15
+ NON_BILLABLE = 0
12
16
 
13
17
  def initialize(config = Configuration.instance)
14
18
  @config = config
@@ -49,32 +53,43 @@ module Ptimelog
49
53
  end
50
54
 
51
55
  def valid?
52
- @start_time && !hidden?
56
+ @start_time && duration.positive? && !hidden?
53
57
  end
54
58
 
55
59
  def hidden?
56
- @description =~ /\*\*$/ # hide lunch and breaks
60
+ @description.to_s.end_with?('**') # hide lunch and breaks
61
+ end
62
+
63
+ def billable?
64
+ @billable == BILLABLE
57
65
  end
58
66
 
59
67
  def infer_ptime_settings
60
- @account = infer_account
61
- @billable = infer_billable
68
+ return if hidden?
69
+ return unless @script.inferer(script_name).exist?
70
+
71
+ @account, @billable = infer_account_and_billable
72
+ end
73
+
74
+ def duration
75
+ (Time.parse(@finish_time) - Time.parse(@start_time)).to_i
62
76
  end
63
77
 
64
78
  def to_s
79
+ billable = billable? ? '($)' : nil
80
+ tag_list = Array(@tags).compact
81
+
82
+ tags = tag_list.join(' ') if tag_list.any?
83
+ desc = [@ticket, @description].compact.join(': ')
84
+ acc = [@account, billable].compact.join(' ') if @account
85
+
65
86
  [
66
- @start_time, '-', @finish_time,
67
- [
68
- @ticket,
69
- @description,
70
- @tags,
71
- @account,
72
- ].compact.join(' : '),
87
+ @start_time, '-', @finish_time, '∴',
88
+ [desc, tags, acc].compact.join(' ∴ '),
73
89
  ].compact.join(' ')
74
90
  end
75
91
 
76
92
  # make sortable/def <=>
77
- # duration if start and finish is set
78
93
 
79
94
  private
80
95
 
@@ -92,24 +107,22 @@ module Ptimelog
92
107
  end.map { |part| part.to_s.rjust(2, '0') }.join(':')
93
108
  end
94
109
 
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?
110
+ def script_name
111
+ @script_name ||= @tags.to_a.first.to_s
112
+ end
102
113
 
103
- cmd = %(#{parser} "#{@ticket}" "#{@description}" #{@tags[1..-1].map(&:inspect).join(' ')})
104
- `#{cmd}`.chomp # maybe only execute if parser is in correct dir?
114
+ def script_args
115
+ @script_args ||= @tags.to_a[1..].to_a.map(&:inspect).join(' ')
105
116
  end
106
117
 
107
- def infer_billable
108
- script = @script.billable
118
+ def infer_account_and_billable
119
+ script = @script.inferer(script_name)
120
+
121
+ cmd = %(#{script} "#{@ticket}" "#{@description}" #{script_args})
109
122
 
110
- return 1 unless script.exist?
123
+ account, billable = `#{cmd}`.chomp.split
111
124
 
112
- `#{script} #{@account}`.chomp == 'true' ? 1 : 0
125
+ [account, (billable == 'true' ? BILLABLE : NON_BILLABLE)]
113
126
  end
114
127
  end
115
128
  end
@@ -9,17 +9,39 @@ module Ptimelog
9
9
  named_date(arg) || :all
10
10
  end
11
11
 
12
- def named_date(date)
12
+ def named_date(date) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/AbcSize
13
13
  case date.to_s
14
- when 'yesterday' then Date.today.prev_day.to_s
14
+ when 'yesterday' then yesterday
15
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
16
+ when 'last', '' then last_entry.to_s || yesterday
17
+ when 'mon', 'monday' then previous_weekday('monday')
18
+ when 'tue', 'tuesday' then previous_weekday('tuesday')
19
+ when 'wed', 'wednesday' then previous_weekday('wednesday')
20
+ when 'thu', 'thursday' then previous_weekday('thursday')
21
+ when 'fri', 'friday' then previous_weekday('friday')
22
+ when 'sat', 'saturday' then previous_weekday('saturday')
23
+ when 'sun', 'sunday' then previous_weekday('sunday')
24
+ when /\d{4}(-\d{2}){2}/ then date.to_s
18
25
  end
19
26
  end
20
27
 
21
28
  private
22
29
 
30
+ def previous_weekday(date)
31
+ Date.today.prev_day(7)
32
+ .step(Date.today.prev_day)
33
+ .find { |d| d.send(:"#{date}?") }
34
+ .to_s
35
+ end
36
+
37
+ def last_entry
38
+ timelog.to_h.keys.compact.sort[-2]
39
+ end
40
+
41
+ def yesterday
42
+ Date.today.prev_day.to_s
43
+ end
44
+
23
45
  def timelog
24
46
  Timelog.load
25
47
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'naught'
4
+ require 'pathname'
5
+
6
+ NullPathname = Naught.build do |config|
7
+ config.impersonate Pathname
8
+ config.predicates_return false
9
+ end
@@ -8,13 +8,11 @@ module Ptimelog
8
8
  @config_dir = config_dir
9
9
  end
10
10
 
11
- def parser(parser_name)
12
- @config_dir.join("parsers/#{parser_name}") # FIXME: security-hole, prevent relative paths!
13
- .expand_path
14
- end
11
+ def inferer(name)
12
+ return NullPathname.new if name.to_s.empty?
13
+ raise if name =~ %r{[\\/]} # prevent relavtive paths, stupidly, FIXME: really check FS
15
14
 
16
- def billable
17
- @config_dir.join('billable').expand_path
15
+ @config_dir.join('inferers').join(name).expand_path
18
16
  end
19
17
  end
20
18
  end
@@ -15,6 +15,14 @@ module Ptimelog
15
15
  def timelog_txt
16
16
  Pathname.new(Configuration.instance[:timelog]).expand_path
17
17
  end
18
+
19
+ def previous_entry
20
+ lines = timelog_txt.readlines.last(2)
21
+ last_line = lines.map(&:chomp).delete_if(&:empty?).last
22
+ last_entry = instance.tokenize(last_line)
23
+
24
+ Entry.from_timelog(last_entry)
25
+ end
18
26
  end
19
27
 
20
28
  def load
@@ -25,6 +33,10 @@ module Ptimelog
25
33
  self.class.timelog_txt
26
34
  end
27
35
 
36
+ def previous_entry
37
+ self.class.previous_entry
38
+ end
39
+
28
40
  def read
29
41
  timelog_txt.read
30
42
  end
@@ -1,6 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Version
3
+ # the version, following semver
4
+ #
5
+ # Someone wanted to have documentation, so here goes...
6
+ #
7
+ # Please note that the version-string is not frozen. Although it of course is,
8
+ # because all strings in this file are frozen. That's what the magic comment
9
+ # at the top of the file does. :gasp:
10
+ #
11
+ # What else? Yeah, the VERSION-constant is part of the module Ptimelog because
12
+ # that is what is described here.
13
+ #
14
+ # So, I truly hope you are happy now, that I documented this file properly. For
15
+ # any remaining questions, please open an issue or even better a pull-request
16
+ # with an improvement. Keep in mind that this is also covered by rspec so I
17
+ # expect (pun intended) 100% test-coverage for any additional code.
4
18
  module Ptimelog
5
- VERSION = '0.5.3'
19
+ VERSION = '0.10.0'
6
20
  end
@@ -22,11 +22,19 @@ Gem::Specification.new do |spec|
22
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
23
  spec.require_paths = ['lib']
24
24
 
25
+ spec.required_ruby_version = '>= 2.7.0'
26
+
27
+ spec.add_dependency 'naught'
28
+
25
29
  spec.add_development_dependency 'bundler'
26
30
  spec.add_development_dependency 'overcommit', '~> 0.45'
27
31
  spec.add_development_dependency 'pry', '~> 0.12'
28
- spec.add_development_dependency 'rake', '~> 10.0'
32
+ spec.add_development_dependency 'rake', '>= 12.3.3'
29
33
  spec.add_development_dependency 'rspec', '~> 3.0'
30
34
  spec.add_development_dependency 'rubocop', '~> 0.50'
35
+ # spec.add_development_dependency 'rubocop-rake'
36
+ # spec.add_development_dependency 'rubocop-rspec'
37
+ # spec.add_development_dependency 'rubocop-packaging'
38
+ # spec.add_development_dependency 'rubocop-performance'
31
39
  spec.add_development_dependency 'timecop', '~> 0.9'
32
40
  end
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ptimelog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthias Viehweger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-15 00:00:00.000000000 Z
11
+ date: 2021-01-27 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: naught
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -56,16 +70,16 @@ dependencies:
56
70
  name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
- - - "~>"
73
+ - - ">="
60
74
  - !ruby/object:Gem::Version
61
- version: '10.0'
75
+ version: 12.3.3
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
- - - "~>"
80
+ - - ">="
67
81
  - !ruby/object:Gem::Version
68
- version: '10.0'
82
+ version: 12.3.3
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rspec
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -116,13 +130,14 @@ executables:
116
130
  extensions: []
117
131
  extra_rdoc_files: []
118
132
  files:
133
+ - ".envrc"
119
134
  - ".gitignore"
120
135
  - ".overcommit.yml"
121
136
  - ".rspec"
122
137
  - ".rubocop.yml"
123
138
  - ".rubocop_todo.yml"
124
- - ".ruby-gemset"
125
139
  - ".ruby-version"
140
+ - ".tool-versions"
126
141
  - ".travis.yml"
127
142
  - Gemfile
128
143
  - LICENSE.txt
@@ -133,13 +148,18 @@ files:
133
148
  - exe/ptimelog
134
149
  - lib/ptimelog.rb
135
150
  - lib/ptimelog/app.rb
151
+ - lib/ptimelog/command/add.rb
136
152
  - lib/ptimelog/command/base.rb
137
153
  - lib/ptimelog/command/edit.rb
138
154
  - lib/ptimelog/command/show.rb
139
155
  - lib/ptimelog/command/upload.rb
156
+ - lib/ptimelog/command/version.rb
140
157
  - lib/ptimelog/configuration.rb
158
+ - lib/ptimelog/day.rb
159
+ - lib/ptimelog/deprecation_warning.rb
141
160
  - lib/ptimelog/entry.rb
142
161
  - lib/ptimelog/named_date.rb
162
+ - lib/ptimelog/null_pathname.rb
143
163
  - lib/ptimelog/script.rb
144
164
  - lib/ptimelog/timelog.rb
145
165
  - lib/ptimelog/version.rb
@@ -156,14 +176,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
156
176
  requirements:
157
177
  - - ">="
158
178
  - !ruby/object:Gem::Version
159
- version: '0'
179
+ version: 2.7.0
160
180
  required_rubygems_version: !ruby/object:Gem::Requirement
161
181
  requirements:
162
182
  - - ">="
163
183
  - !ruby/object:Gem::Version
164
184
  version: '0'
165
185
  requirements: []
166
- rubygems_version: 3.0.3
186
+ rubygems_version: 3.1.4
167
187
  signing_key:
168
188
  specification_version: 4
169
189
  summary: Move time-entries from gTimelog to PuzzleTime
@@ -1 +0,0 @@
1
- gpuzzletime