fewald-worklog 0.2.19 → 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 +95 -73
- data/lib/printer.rb +1 -1
- data/lib/project.rb +10 -2
- data/lib/statistics.rb +1 -1
- data/lib/storage.rb +145 -135
- data/lib/worklog.rb +42 -12
- 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,95 +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
|
-
|
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
|
33
48
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
49
|
+
# Returns true if the entry is an epic, false otherwise.
|
50
|
+
# @return [Boolean]
|
51
|
+
def epic?
|
52
|
+
@epic == true
|
53
|
+
end
|
38
54
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
52
69
|
end
|
53
|
-
end
|
54
70
|
|
55
|
-
|
71
|
+
s = ''
|
56
72
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
73
|
+
s += if epic
|
74
|
+
Rainbow("[EPIC] #{msg}").bg(:white).fg(:black)
|
75
|
+
else
|
76
|
+
msg
|
77
|
+
end
|
62
78
|
|
63
|
-
|
79
|
+
s += " [#{Rainbow(@ticket).fg(:blue)}]" if @ticket
|
64
80
|
|
65
|
-
|
66
|
-
|
81
|
+
# Add tags in brackets if defined.
|
82
|
+
s += ' [' + @tags.map { |tag| "#{tag}" }.join(', ') + ']' if @tags && @tags.size > 0
|
67
83
|
|
68
|
-
|
69
|
-
|
84
|
+
# Add URL in brackets if defined.
|
85
|
+
s += " [#{@url}]" if @url && @url != ''
|
70
86
|
|
71
|
-
|
87
|
+
s += " [#{@project}]" if @project && @project != ''
|
72
88
|
|
73
|
-
|
74
|
-
|
89
|
+
s
|
90
|
+
end
|
75
91
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
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
|
83
99
|
|
84
|
-
def people?
|
85
100
|
# Return true if there are people in the entry.
|
86
101
|
#
|
87
102
|
# @return [Boolean]
|
88
|
-
people
|
89
|
-
|
103
|
+
def people?
|
104
|
+
people.size.positive?
|
105
|
+
end
|
90
106
|
|
91
|
-
|
92
|
-
|
93
|
-
|
107
|
+
# Convert the log entry to YAML format.
|
108
|
+
def to_yaml
|
109
|
+
to_hash.to_yaml
|
110
|
+
end
|
94
111
|
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
98
120
|
end
|
99
121
|
end
|
data/lib/printer.rb
CHANGED
@@ -9,7 +9,7 @@ class Printer
|
|
9
9
|
# Initializes the printer with a list of people.
|
10
10
|
# @param people [Array<Person>] An array of Person objects.
|
11
11
|
def initialize(people = nil)
|
12
|
-
@people =
|
12
|
+
@people = people || {}
|
13
13
|
end
|
14
14
|
|
15
15
|
# Prints a whole day of work log entries.
|
data/lib/project.rb
CHANGED
@@ -17,10 +17,18 @@ module Worklog
|
|
17
17
|
# @return [String, nil] The status of the project, can be nil
|
18
18
|
# Possible values: 'active', 'completed', 'archived', etc.
|
19
19
|
# Indicates the current state of the project.
|
20
|
+
# @!attribute [rw] entries
|
21
|
+
# These entries are related to the work done on this project.
|
22
|
+
# Entries are populated dynamically when processing daily logs.
|
23
|
+
# They are not stored in the project itself.
|
24
|
+
# @return [Array<LogEntry>] An array of log entries associated with the project.
|
20
25
|
# @!attribute [rw] last_activity
|
21
|
-
#
|
26
|
+
# The last activity is not stored in the project itself.
|
27
|
+
# Instead, it is updated dynamically when processing daily logs.
|
28
|
+
# It represents the most recent log entry time for this project.
|
29
|
+
# @return [Date, nil] The last activity date or nil if not set.
|
22
30
|
class Project
|
23
|
-
attr_accessor :key, :name, :description, :start_date, :end_date, :status, :last_activity
|
31
|
+
attr_accessor :key, :name, :description, :start_date, :end_date, :status, :entries, :last_activity
|
24
32
|
|
25
33
|
# Creates a new Project instance from a hash of attributes.
|
26
34
|
# @param hash [Hash] A hash containing project attributes
|
data/lib/statistics.rb
CHANGED
data/lib/storage.rb
CHANGED
@@ -6,176 +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
|
-
|
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
|
132
141
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
142
151
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
148
157
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
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)
|
154
163
|
|
155
|
-
|
156
|
-
|
164
|
+
YAML.load_file(people_file, permitted_classes: [Person])
|
165
|
+
end
|
157
166
|
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
164
174
|
end
|
165
|
-
end
|
166
175
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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?
|
171
180
|
|
172
|
-
|
173
|
-
|
181
|
+
Dir.mkdir(@config.storage_path) unless Dir.exist?(@config.storage_path)
|
182
|
+
end
|
174
183
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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
|
180
190
|
end
|
181
191
|
end
|
data/lib/worklog.rb
CHANGED
@@ -54,6 +54,9 @@ module Worklog
|
|
54
54
|
# Bootstrap the worklog application.
|
55
55
|
def bootstrap
|
56
56
|
@storage.create_default_folder
|
57
|
+
|
58
|
+
# Load all people as they're used in multiple/most of the methods.
|
59
|
+
@people = @storage.load_people_hash
|
57
60
|
end
|
58
61
|
|
59
62
|
# Add new entry to the work log.
|
@@ -89,8 +92,7 @@ module Worklog
|
|
89
92
|
|
90
93
|
@storage.write_log(@storage.filepath(options[:date]), daily_log)
|
91
94
|
|
92
|
-
|
93
|
-
(new_entry.people - people_hash.keys).each do |handle|
|
95
|
+
(new_entry.people - @people.keys).each do |handle|
|
94
96
|
WorkLogger.warn "Person with handle #{handle} not found. Consider adding them to people.yaml"
|
95
97
|
end
|
96
98
|
|
@@ -130,8 +132,7 @@ module Worklog
|
|
130
132
|
# worklog.show(from: '2023-10-01', to: '2023-10-31')
|
131
133
|
# worklog.show(date: '2023-10-01')
|
132
134
|
def show(options = {})
|
133
|
-
|
134
|
-
printer = Printer.new(people)
|
135
|
+
printer = Printer.new(@people)
|
135
136
|
|
136
137
|
start_date, end_date = start_end_date(options)
|
137
138
|
|
@@ -145,17 +146,16 @@ module Worklog
|
|
145
146
|
end
|
146
147
|
end
|
147
148
|
|
149
|
+
# Show all known people and details about a specific person.
|
148
150
|
def people(person = nil, _options = {})
|
149
|
-
all_people = @storage.load_people!
|
150
|
-
people_map = all_people.to_h { |p| [p.handle, p] }
|
151
151
|
all_logs = @storage.all_days
|
152
152
|
|
153
153
|
if person
|
154
|
-
unless
|
154
|
+
unless @people.key?(person)
|
155
155
|
WorkLogger.error Rainbow("No person found with handle #{person}.").red
|
156
156
|
return
|
157
157
|
end
|
158
|
-
person_detail(all_logs,
|
158
|
+
person_detail(all_logs, @people, @people[person.strip])
|
159
159
|
else
|
160
160
|
puts 'People mentioned in the work log:'
|
161
161
|
|
@@ -169,8 +169,8 @@ module Worklog
|
|
169
169
|
mentions = mentions.to_a.sort_by { |handle, _| handle }
|
170
170
|
|
171
171
|
mentions.each do |handle, v|
|
172
|
-
if
|
173
|
-
person =
|
172
|
+
if @people.key?(handle)
|
173
|
+
person = @people[handle]
|
174
174
|
print "#{Rainbow(person.name).gold} (#{handle})"
|
175
175
|
print " (#{person.team})" if person.team
|
176
176
|
else
|
@@ -203,13 +203,43 @@ module Worklog
|
|
203
203
|
def projects(_options = {})
|
204
204
|
project_storage = ProjectStorage.new(@config)
|
205
205
|
projects = project_storage.load_projects
|
206
|
-
|
206
|
+
|
207
|
+
# Load all entries to find latest activity for each project
|
208
|
+
@storage.all_days.each do |daily_log|
|
209
|
+
daily_log.entries.each do |entry|
|
210
|
+
if projects.key?(entry.project)
|
211
|
+
project = projects[entry.project]
|
212
|
+
project.entries ||= []
|
213
|
+
project.entries << entry
|
214
|
+
# Update last activity date if entry time is more recent
|
215
|
+
project.last_activity = entry.time if project.last_activity.nil? || entry.time > project.last_activity
|
216
|
+
else
|
217
|
+
WorkLogger.debug "Project with key '#{entry.project}' not found in projects. Skipping."
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
print_projects(projects)
|
222
|
+
end
|
223
|
+
|
224
|
+
def print_projects(projects)
|
225
|
+
puts Rainbow('Active Projects:').gold
|
207
226
|
projects.each_value do |project|
|
227
|
+
# Sort entries by descending time
|
228
|
+
project.entries.sort_by!(&:time).reverse!
|
229
|
+
|
208
230
|
puts "#{Rainbow(project.name).gold} (#{project.key})"
|
209
231
|
puts " Description: #{project.description}" if project.description
|
210
232
|
puts " Start date: #{project.start_date.strftime('%b %d, %Y')}" if project.start_date
|
211
233
|
puts " End date: #{project.end_date.strftime('%b %d, %Y')}" if project.end_date
|
212
234
|
puts " Status: #{project.status}" if project.status
|
235
|
+
puts " Last activity: #{project.last_activity.strftime('%b %d, %Y')}" if project.last_activity
|
236
|
+
|
237
|
+
next unless project.entries && !project.entries.empty?
|
238
|
+
|
239
|
+
puts " Last #{[project.entries&.size, 3].min} entries:"
|
240
|
+
puts " #{project.entries.last(3).map do |e|
|
241
|
+
"#{e.time.strftime('%b %d, %Y')} #{e.message_string(@people)}"
|
242
|
+
end.join("\n ")}"
|
213
243
|
end
|
214
244
|
puts 'No projects found.' if projects.empty?
|
215
245
|
end
|
@@ -254,7 +284,7 @@ module Worklog
|
|
254
284
|
# @example
|
255
285
|
# worklog.tag_detail('example_tag', from: '2023-10-01', to: '2023-10-31')
|
256
286
|
def tag_detail(tag, options)
|
257
|
-
printer = Printer.new(@
|
287
|
+
printer = Printer.new(@people)
|
258
288
|
start_date, end_date = start_end_date(options)
|
259
289
|
|
260
290
|
@storage.days_between(start_date, end_date).each do |daily_log|
|