fewald-worklog 0.2.20 → 0.2.21
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 +4 -4
- data/.version +1 -1
- data/lib/cli.rb +1 -1
- data/lib/daily_log.rb +49 -47
- data/lib/log_entry.rb +98 -96
- data/lib/statistics.rb +1 -1
- data/lib/storage.rb +145 -136
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a799c589dedc9f7ed68ef9b2acfd8ad08e29f3fb5f3a26d0a90dee1320ee164e
|
4
|
+
data.tar.gz: 9e38d05b4303b41cf7ae2016d23990290ef2b41eba34db9b8c53f4a7587a84d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a9cc6c2ef435e43390491c2d31793c4e2ae8febd54bdd68ea535decb109defd70538bdaaaa1dc70d660c0f1193e9786d4bab66cf1a0619bf5166146b1c8eb95c
|
7
|
+
data.tar.gz: aa43dfa148848d4161a791105934e36c9caf21929d4e978e3b20468fe076b288b555137cf7b74f06fb17157f9312fcc40050894e494c176334679d61033fa198
|
data/.version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.2.
|
1
|
+
0.2.21
|
data/lib/cli.rb
CHANGED
@@ -31,7 +31,7 @@ class WorklogCLI < Thor
|
|
31
31
|
# Initialize the CLI with the given arguments, options, and configuration
|
32
32
|
def initialize(args = [], options = {}, config = {})
|
33
33
|
@config = Worklog::Configuration.load
|
34
|
-
@storage = Storage.new(@config)
|
34
|
+
@storage = Worklog::Storage.new(@config)
|
35
35
|
super
|
36
36
|
end
|
37
37
|
|
data/lib/daily_log.rb
CHANGED
@@ -2,52 +2,54 @@
|
|
2
2
|
|
3
3
|
require 'hash'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
people
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
5
|
+
module Worklog
|
6
|
+
# DailyLog is a container for a day's work log.
|
7
|
+
class DailyLog
|
8
|
+
# Container for a day's work log.
|
9
|
+
include Hashify
|
10
|
+
|
11
|
+
# Represents a single day's work log.
|
12
|
+
attr_accessor :date, :entries
|
13
|
+
|
14
|
+
def initialize(params = {})
|
15
|
+
@date = params[:date]
|
16
|
+
@entries = params[:entries]
|
17
|
+
end
|
18
|
+
|
19
|
+
# Returns true if there are people mentioned in any entry of the current day.
|
20
|
+
#
|
21
|
+
# @return [Boolean] true if there are people mentioned, false otherwise.
|
22
|
+
def people?
|
23
|
+
people.size.positive?
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns a hash of people mentioned in the log for the current day
|
27
|
+
# with the number of times they are mentioned.
|
28
|
+
# People are defined as words starting with @ or ~.
|
29
|
+
#
|
30
|
+
# @return [Hash<String, Integer>]
|
31
|
+
def people
|
32
|
+
entries.map { |entry| entry.people.to_a }.flatten.tally
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns a sorted list of tags used in the entries for the current day.
|
36
|
+
#
|
37
|
+
# @return [Array<String>]
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# log = DailyLog.new(date: Date.today,
|
41
|
+
# entries: [LogEntry.new(message: "Work on something", tags: ['work', 'project'])])
|
42
|
+
# log.tags # => ["project", "work"]
|
43
|
+
def tags
|
44
|
+
entries.flat_map(&:tags).uniq.sort
|
45
|
+
end
|
46
|
+
|
47
|
+
# Equals method to compare two DailyLog objects.
|
48
|
+
#
|
49
|
+
# @param other [DailyLog] the other DailyLog object to compare with
|
50
|
+
# @return [Boolean] true if both DailyLog objects have the same date and entries, false otherwise
|
51
|
+
def ==(other)
|
52
|
+
date == other.date && entries == other.entries
|
53
|
+
end
|
52
54
|
end
|
53
55
|
end
|
data/lib/log_entry.rb
CHANGED
@@ -5,115 +5,117 @@ require 'rainbow'
|
|
5
5
|
require 'daily_log'
|
6
6
|
require 'hash'
|
7
7
|
|
8
|
-
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
8
|
+
module Worklog
|
9
|
+
# A single log entry in a DailyLog.
|
10
|
+
# @see DailyLog
|
11
|
+
# @!attribute [rw] time
|
12
|
+
# @return [DateTime] the date and time of the log entry.
|
13
|
+
# @!attribute [rw] tags
|
14
|
+
# @return [Array<String>] the tags associated with the log entry.
|
15
|
+
# @!attribute [rw] ticket
|
16
|
+
# @return [String] the ticket associated with the log entry.
|
17
|
+
# @!attribute [rw] url
|
18
|
+
# @return [String] the URL associated with the log entry.
|
19
|
+
# @!attribute [rw] epic
|
20
|
+
# @return [Boolean] whether the log entry is an epic.
|
21
|
+
# @!attribute [rw] message
|
22
|
+
# @return [String] the message of the log entry.
|
23
|
+
# @!attribute [rw] project
|
24
|
+
# @return [String] the project associated with the log entry.
|
25
|
+
class LogEntry
|
26
|
+
PERSON_REGEX = /(?:\s|^)[~@](\w+)/
|
27
|
+
|
28
|
+
include Hashify
|
29
|
+
|
30
|
+
attr_accessor :time, :tags, :ticket, :url, :epic, :message, :project
|
31
|
+
|
32
|
+
attr_reader :day
|
33
|
+
|
34
|
+
def initialize(params = {})
|
35
|
+
@time = params[:time]
|
36
|
+
# If tags are nil, set to empty array.
|
37
|
+
# This is similar to the CLI default value.
|
38
|
+
@tags = params[:tags] || []
|
39
|
+
@ticket = params[:ticket]
|
40
|
+
@url = params[:url] || ''
|
41
|
+
@epic = params[:epic]
|
42
|
+
@message = params[:message]
|
43
|
+
@project = params[:project]
|
44
|
+
|
45
|
+
# Back reference to the day
|
46
|
+
@day = params[:day] || nil
|
47
|
+
end
|
47
48
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
49
|
+
# Returns true if the entry is an epic, false otherwise.
|
50
|
+
# @return [Boolean]
|
51
|
+
def epic?
|
52
|
+
@epic == true
|
53
|
+
end
|
53
54
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
55
|
+
# Returns the message string with formatting without the time.
|
56
|
+
# @param known_people Hash[String, Person] A hash of people with their handles as keys.
|
57
|
+
def message_string(known_people = nil)
|
58
|
+
# replace all mentions of people with their names.
|
59
|
+
msg = @message.dup
|
60
|
+
people.each do |person|
|
61
|
+
next unless known_people && known_people[person]
|
62
|
+
|
63
|
+
msg.gsub!(/[~@]#{person}/) do |match|
|
64
|
+
s = ''
|
65
|
+
s += ' ' if match[0] == ' '
|
66
|
+
s += "#{Rainbow(known_people[person].name).underline} (~#{person})" if known_people && known_people[person]
|
67
|
+
s
|
68
|
+
end
|
67
69
|
end
|
68
|
-
end
|
69
70
|
|
70
|
-
|
71
|
+
s = ''
|
71
72
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
73
|
+
s += if epic
|
74
|
+
Rainbow("[EPIC] #{msg}").bg(:white).fg(:black)
|
75
|
+
else
|
76
|
+
msg
|
77
|
+
end
|
77
78
|
|
78
|
-
|
79
|
+
s += " [#{Rainbow(@ticket).fg(:blue)}]" if @ticket
|
79
80
|
|
80
|
-
|
81
|
-
|
81
|
+
# Add tags in brackets if defined.
|
82
|
+
s += ' [' + @tags.map { |tag| "#{tag}" }.join(', ') + ']' if @tags && @tags.size > 0
|
82
83
|
|
83
|
-
|
84
|
-
|
84
|
+
# Add URL in brackets if defined.
|
85
|
+
s += " [#{@url}]" if @url && @url != ''
|
85
86
|
|
86
|
-
|
87
|
+
s += " [#{@project}]" if @project && @project != ''
|
87
88
|
|
88
|
-
|
89
|
-
|
89
|
+
s
|
90
|
+
end
|
90
91
|
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
92
|
+
def people
|
93
|
+
# Return people that are mentioned in the entry. People are defined as character sequences
|
94
|
+
# starting with @ or ~. Whitespaces are used to separate people. Punctuation is ignored.
|
95
|
+
# Empty set if no people are mentioned.
|
96
|
+
# @return [Set<String>]
|
97
|
+
@message.scan(PERSON_REGEX).flatten.uniq.sort.to_set
|
98
|
+
end
|
98
99
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
100
|
+
# Return true if there are people in the entry.
|
101
|
+
#
|
102
|
+
# @return [Boolean]
|
103
|
+
def people?
|
104
|
+
people.size.positive?
|
105
|
+
end
|
105
106
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
107
|
+
# Convert the log entry to YAML format.
|
108
|
+
def to_yaml
|
109
|
+
to_hash.to_yaml
|
110
|
+
end
|
110
111
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
112
|
+
# Compare two log entries for equality.
|
113
|
+
#
|
114
|
+
# @param other [LogEntry] The other log entry to compare against.
|
115
|
+
# @return [Boolean] True if the log entries are equal, false otherwise.
|
116
|
+
def ==(other)
|
117
|
+
time == other.time && tags == other.tags && ticket == other.ticket && url == other.url &&
|
118
|
+
epic == other.epic && message == other.message
|
119
|
+
end
|
118
120
|
end
|
119
121
|
end
|
data/lib/statistics.rb
CHANGED
data/lib/storage.rb
CHANGED
@@ -6,177 +6,186 @@ require 'log_entry'
|
|
6
6
|
require 'worklogger'
|
7
7
|
require 'person'
|
8
8
|
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
class LogNotFoundError < StandardError; end
|
9
|
+
# Alias for classes to handle existing log entries
|
10
|
+
DailyLog = Worklog::DailyLog
|
11
|
+
LogEntry = Worklog::LogEntry
|
13
12
|
|
14
|
-
|
13
|
+
module Worklog
|
14
|
+
# Handles storage of daily logs and people
|
15
|
+
class Storage
|
16
|
+
# LogNotFoundError is raised when a log file is not found
|
17
|
+
class LogNotFoundError < StandardError; end
|
15
18
|
|
16
|
-
|
17
|
-
LOG_PATTERN = /\d{4}-\d{2}-\d{2}#{FILE_SUFFIX}\z/
|
19
|
+
FILE_SUFFIX = '.yaml'
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
end
|
21
|
+
# Regular expression to match daily log file names
|
22
|
+
LOG_PATTERN = /\d{4}-\d{2}-\d{2}#{FILE_SUFFIX}\z/
|
22
23
|
|
23
|
-
|
24
|
-
|
25
|
-
|
24
|
+
def initialize(config)
|
25
|
+
@config = config
|
26
|
+
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
return [] unless folder_exists?
|
28
|
+
def folder_exists?
|
29
|
+
Dir.exist?(@config.storage_path)
|
30
|
+
end
|
31
31
|
|
32
|
-
logs
|
33
|
-
|
34
|
-
|
32
|
+
# Return all logs for all available days
|
33
|
+
# @return [Array<DailyLog>] List of all logs
|
34
|
+
def all_days
|
35
|
+
return [] unless folder_exists?
|
35
36
|
|
36
|
-
logs
|
37
|
-
|
37
|
+
logs = []
|
38
|
+
Dir.glob(File.join(@config.storage_path, "*#{FILE_SUFFIX}")).map do |file|
|
39
|
+
next unless file.match?(LOG_PATTERN)
|
38
40
|
|
39
|
-
|
40
|
-
|
41
|
+
logs << load_log(file)
|
42
|
+
end
|
43
|
+
|
44
|
+
logs
|
45
|
+
end
|
41
46
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
47
|
+
# Return all tags as a set
|
48
|
+
# @return [Set<String>] Set of all tags
|
49
|
+
def tags
|
50
|
+
logs = all_days
|
51
|
+
tags = Set[]
|
52
|
+
logs.each do |log|
|
53
|
+
log.entries.each do |entry|
|
54
|
+
next unless entry.tags
|
55
|
+
|
56
|
+
entry.tags.each do |tag|
|
57
|
+
tags << tag
|
58
|
+
end
|
53
59
|
end
|
54
60
|
end
|
61
|
+
tags
|
55
62
|
end
|
56
|
-
tags
|
57
|
-
end
|
58
63
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
64
|
+
# Return days between start_date and end_date
|
65
|
+
# If end_date is nil, return logs from start_date to today
|
66
|
+
#
|
67
|
+
# @param [Date] start_date The start date, inclusive
|
68
|
+
# @param [Date] end_date The end date, inclusive
|
69
|
+
# @param [Boolean] epics_only If true, only return logs with epic entries
|
70
|
+
# @param [Array<String>] tags_filter If provided, only return logs with entries that have at least one of the tags
|
71
|
+
# @return [Array<DailyLog>] List of logs
|
72
|
+
def days_between(start_date, end_date = nil, epics_only = nil, tags_filter = nil)
|
73
|
+
return [] unless folder_exists?
|
74
|
+
|
75
|
+
logs = []
|
76
|
+
end_date = Date.today if end_date.nil?
|
77
|
+
|
78
|
+
return [] if start_date > end_date
|
79
|
+
|
80
|
+
while start_date <= end_date
|
81
|
+
if File.exist?(filepath(start_date))
|
82
|
+
tmp_logs = load_log!(filepath(start_date))
|
83
|
+
tmp_logs.entries.keep_if { |entry| entry.epic? } if epics_only
|
84
|
+
|
85
|
+
if tags_filter
|
86
|
+
# Safeguard against entries without any tags, not just empty array
|
87
|
+
tmp_logs.entries.keep_if { |entry| entry.tags && (entry.tags & tags_filter).size > 0 }
|
88
|
+
end
|
89
|
+
|
90
|
+
logs << tmp_logs if tmp_logs.entries.length > 0
|
83
91
|
end
|
84
92
|
|
85
|
-
|
93
|
+
start_date += 1
|
86
94
|
end
|
95
|
+
logs
|
96
|
+
end
|
87
97
|
|
88
|
-
|
98
|
+
# Create file for a new day if it does not exist
|
99
|
+
# @param [Date] date The date, used as the file name.
|
100
|
+
def create_file_skeleton(date)
|
101
|
+
File.write(filepath(date), YAML.dump(DailyLog.new(date:, entries: []))) unless File.exist?(filepath(date))
|
89
102
|
end
|
90
|
-
logs
|
91
|
-
end
|
92
103
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
104
|
+
def load_log(file)
|
105
|
+
load_log!(file)
|
106
|
+
rescue LogNotFoundError
|
107
|
+
WorkLogger.error "No work log found for #{file}. Aborting."
|
108
|
+
nil
|
109
|
+
end
|
98
110
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
nil
|
104
|
-
end
|
111
|
+
def load_log!(file)
|
112
|
+
WorkLogger.debug "Loading file #{file}"
|
113
|
+
|
114
|
+
# Alias DailyLog to Worklog::DailyLog
|
105
115
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
116
|
+
begin
|
117
|
+
log = YAML.load_file(file, permitted_classes: [Date, Time, DailyLog, LogEntry])
|
118
|
+
log.entries.each do |entry|
|
119
|
+
entry.time = Time.parse(entry.time) unless entry.time.respond_to?(:strftime)
|
120
|
+
end
|
121
|
+
log
|
122
|
+
rescue Errno::ENOENT
|
123
|
+
raise LogNotFoundError
|
112
124
|
end
|
113
|
-
log
|
114
|
-
rescue Errno::ENOENT
|
115
|
-
raise LogNotFoundError
|
116
125
|
end
|
117
|
-
end
|
118
126
|
|
119
|
-
|
120
|
-
|
127
|
+
def write_log(file, daily_log)
|
128
|
+
WorkLogger.debug "Writing to file #{file}"
|
121
129
|
|
122
|
-
|
123
|
-
|
130
|
+
File.open(file, 'w') do |f|
|
131
|
+
f.puts daily_log.to_yaml
|
132
|
+
end
|
124
133
|
end
|
125
|
-
end
|
126
134
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
135
|
+
# Load a single log file and return its entries
|
136
|
+
def load_single_log_file(file, headline = true)
|
137
|
+
daily_log = load_log!(file)
|
138
|
+
puts "Work log for #{Rainbow(daily_log.date).gold}:" if headline
|
139
|
+
daily_log.entries
|
140
|
+
end
|
133
141
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
142
|
+
# Load all people from the people file, or return an empty array if the file does not exist
|
143
|
+
#
|
144
|
+
# @return [Array<Person>] List of people
|
145
|
+
def load_people
|
146
|
+
load_people!
|
147
|
+
rescue Errno::ENOENT
|
148
|
+
WorkLogger.info 'Unable to load people.'
|
149
|
+
[]
|
150
|
+
end
|
143
151
|
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
152
|
+
# Load all people from the people file and return them as a hash with handle as key
|
153
|
+
# @return [Hash<String, Person>] Hash of people with handle as key
|
154
|
+
def load_people_hash
|
155
|
+
load_people.to_h { |person| [person.handle, person] }
|
156
|
+
end
|
149
157
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
158
|
+
# Load all people from the people file
|
159
|
+
# @return [Array<Person>] List of people
|
160
|
+
def load_people!
|
161
|
+
people_file = File.join(@config.storage_path, 'people.yaml')
|
162
|
+
return [] unless File.exist?(people_file)
|
155
163
|
|
156
|
-
|
157
|
-
|
164
|
+
YAML.load_file(people_file, permitted_classes: [Person])
|
165
|
+
end
|
158
166
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
167
|
+
# Write people to the people file
|
168
|
+
# @param [Array<Person>] people List of people
|
169
|
+
def write_people!(people)
|
170
|
+
people_file = File.join(@config.storage_path, 'people.yaml')
|
171
|
+
File.open(people_file, 'w') do |f|
|
172
|
+
f.puts people.to_yaml
|
173
|
+
end
|
165
174
|
end
|
166
|
-
end
|
167
175
|
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
176
|
+
# Create folder if not exists already.
|
177
|
+
def create_default_folder
|
178
|
+
# Do nothing if the storage path is not the default path
|
179
|
+
return unless @config.default_storage_path?
|
172
180
|
|
173
|
-
|
174
|
-
|
181
|
+
Dir.mkdir(@config.storage_path) unless Dir.exist?(@config.storage_path)
|
182
|
+
end
|
175
183
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
184
|
+
# Construct filepath for a given date.
|
185
|
+
# @param [Date] date The date
|
186
|
+
# @return [String] The filepath
|
187
|
+
def filepath(date)
|
188
|
+
File.join(@config.storage_path, "#{date}#{FILE_SUFFIX}")
|
189
|
+
end
|
181
190
|
end
|
182
191
|
end
|