ptimelog 0.5.3 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
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