fewald-worklog 0.1.11 → 0.1.13

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: 1a7119faa8d6fc12fd4ed7c98c9b3ab686b7e18426e96c60482d07421f8bb30a
4
- data.tar.gz: '03863c72e41d80e0333d6479fda9290a29f68cc93da989d0e4426b9b09a3d21d'
3
+ metadata.gz: 72fb884d4adc03a89030b3c7811070131c814b5ddc7f9e4bb2846b400fedb678
4
+ data.tar.gz: 24ff8173ce145bb1da3c1d9758626acb5c93c1b31bd4fadb785bd5dbed283ebe
5
5
  SHA512:
6
- metadata.gz: 98df77412956964c7da349fcd484932e1e85e99f1fa1990aac72115c4b2c1780523ea27d67470b2d0578146b2414cbd2f46e2237f55e644013787d903c64e9f4
7
- data.tar.gz: 3ebe81dbf6fc09bdb0c112d32bc83d880dd444bef59016b4786b560b84917969116c798a6217446528fd44011be205c1ad012680afccff2a21177e77d145ac54
6
+ metadata.gz: db4fe81d42e70d1f8209d939bc676a3655a5fdcf8a095c048c551ea35f0057dc7e7b04bd8e8ee86ec90f9b65c7231eba2dac205a9dc8acc17e32ce0841bcb267
7
+ data.tar.gz: 93153a298dd1b4034bf7b632f117dbddc75f8d36bb741b90e230a14859ed40c7e18d44c0d1478253df3c937f7c465326d799ec9bbede358dead40ad551a4b985
data/.version CHANGED
@@ -1 +1 @@
1
- 0.1.11
1
+ 0.1.13
data/worklog/cli.rb CHANGED
@@ -10,22 +10,32 @@ require 'logger'
10
10
 
11
11
  require_relative 'worklog'
12
12
  require 'date_parser'
13
- require 'printer'
14
- require 'statistics'
15
- require 'storage'
16
- require 'webserver'
17
- require_relative 'summary'
13
+ require_relative 'configuration'
18
14
  require_relative 'editor'
15
+ require_relative 'printer'
16
+ require_relative 'statistics'
17
+ require_relative 'storage'
19
18
  require_relative 'string_helper'
19
+ require_relative 'summary'
20
20
  require_relative 'version'
21
+ require_relative 'webserver'
21
22
 
22
23
  # CLI for the work log application
23
24
  class WorklogCLI < Thor
25
+ attr_accessor :config, :storage
26
+
24
27
  include StringHelper
25
28
  class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
26
29
 
27
30
  package_name 'Worklog'
28
31
 
32
+ # Initialize the CLI with the given arguments, options, and configuration
33
+ def initialize(args = [], options = {}, config = {})
34
+ @config = load_configuration
35
+ @storage = Storage.new(@config)
36
+ super
37
+ end
38
+
29
39
  def self.exit_on_failure?
30
40
  true
31
41
  end
@@ -45,71 +55,22 @@ class WorklogCLI < Thor
45
55
  option :url, type: :string, desc: 'URL to associate with the entry'
46
56
  option :epic, type: :boolean, default: false, desc: 'Mark the entry as an epic'
47
57
  def add(message)
48
- set_log_level
49
-
50
- # Remove leading and trailing whitespaces
51
- # Raise an error if the message is empty
52
- message = message.strip
53
- raise ArgumentError, 'Message cannot be empty' if message.empty?
54
-
55
- date = Date.strptime(options[:date], '%Y-%m-%d')
56
- time = Time.strptime(options[:time], '%H:%M:%S')
57
- Storage.create_file_skeleton(date)
58
-
59
- daily_log = Storage.load_log!(Storage.filepath(date))
60
- daily_log.entries << LogEntry.new(time:, tags: options[:tags], ticket: options[:ticket], url: options[:url],
61
- epic: options[:epic], message:)
62
-
63
- # Sort by time in case an entry was added later out of order.
64
- daily_log.entries.sort_by!(&:time)
65
-
66
- Storage.write_log(Storage.filepath(options[:date]), daily_log)
67
-
68
- WorkLogger.info Rainbow("Added entry on #{options[:date]}: #{message}").green
58
+ worklog = Worklog.new
59
+ worklog.add(message, options)
69
60
  end
70
61
 
71
- desc 'edit', 'Edit a specified day in the work log'
62
+ desc 'edit', 'Edit a day in the work log. By default, the current date is used.'
72
63
  option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
73
64
  def edit
74
- set_log_level
75
-
76
- date = Date.strptime(options[:date], '%Y-%m-%d')
77
-
78
- # Load existing log
79
- log = Storage.load_log(Storage.filepath(date))
80
- unless log
81
- WorkLogger.error "No work log found for #{options[:date]}. Aborting."
82
- exit 1
83
- end
84
-
85
- txt = Editor::EDITOR_PREAMBLE.result_with_hash(content: YAML.dump(log))
86
- return_val = Editor.open_editor(txt)
87
-
88
- Storage.write_log(Storage.filepath(date),
89
- YAML.load(return_val, permitted_classes: [Date, Time, DailyLog, LogEntry]))
90
- WorkLogger.info Rainbow("Updated work log for #{options[:date]}").green
65
+ worklog = Worklog.new
66
+ worklog.edit(options)
91
67
  end
92
68
 
93
69
  desc 'remove', 'Remove last entry from the log'
94
70
  option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
95
71
  def remove
96
- set_log_level
97
-
98
- date = Date.strptime(options[:date], '%Y-%m-%d')
99
- unless File.exist?(Storage.filepath(date))
100
- WorkLogger.error Rainbow("No work log found for #{options[:date]}. Aborting.").red
101
- exit 1
102
- end
103
-
104
- daily_log = Storage.load_log!(Storage.filepath(options[:date]))
105
- if daily_log.entries.empty?
106
- WorkLogger.error Rainbow("No entries found for #{options[:date]}. Aborting.").red
107
- exit 1
108
- end
109
-
110
- removed_entry = daily_log.entries.pop
111
- Storage.write_log(Storage.filepath(date), daily_log)
112
- WorkLogger.info Rainbow("Removed entry: #{removed_entry.message}").green
72
+ worklog = Worklog.new
73
+ worklog.remove(options)
113
74
  end
114
75
 
115
76
  desc 'show', 'Show the work log for a specific date or a range of dates. Defaults to todays date.'
@@ -132,69 +93,32 @@ class WorklogCLI < Thor
132
93
  option :epics_only, type: :boolean, default: false, desc: 'Show only entries that are marked as epic'
133
94
  option :tags, type: :array, default: [], desc: 'Filter entries by tags. Tags are treated as an OR condition.'
134
95
  def show
135
- set_log_level
136
-
137
- start_date, end_date = start_end_date(options)
138
-
139
- entries = Storage.days_between(start_date, end_date)
140
- if entries.empty?
141
- Printer.no_entries(start_date, end_date)
142
- else
143
- entries.each do |entry|
144
- Printer.print_day(entry, entries.size > 1, options[:epics_only])
145
- end
146
- end
96
+ worklog = Worklog.new
97
+ worklog.show(options)
147
98
  end
148
99
 
149
100
  desc 'people', 'Show all people mentioned in the work log'
150
101
  def people
151
- set_log_level
152
-
153
- puts 'People mentioned in the work log:'
154
-
155
- mentions = {}
156
- all_logs = Storage.all_days
157
- all_logs.map(&:people).each do |people|
158
- mentions.merge!(people) { |_key, oldval, newval| oldval + newval }
159
- end
160
- mentions.each { |k, v| puts "#{Rainbow(k).gold}: #{v} #{pluralize(v, 'occurrence')}" }
102
+ worklog = Worklog.new
103
+ worklog.people(options)
161
104
  end
162
105
 
163
106
  desc 'tags', 'Show all tags used in the work log'
164
107
  def tags
165
- set_log_level
166
-
167
- all_logs = Storage.all_days
168
-
169
- puts Rainbow('Tags used in the work log:').gold
170
-
171
- # Count all tags used in the work log
172
- tags = all_logs.map(&:entries).flatten.map(&:tags).flatten.compact.tally
173
-
174
- # Determine length of longest tag for formatting
175
- max_len = tags.keys.map(&:length).max
176
-
177
- tags.each { |k, v| puts "#{Rainbow(k.ljust(max_len)).gold}: #{v} #{pluralize(v, 'occurrence')}" }
108
+ worklog = Worklog.new
109
+ worklog.tags(options)
178
110
  end
179
111
 
180
112
  desc 'server', 'Start the work log server'
181
113
  def server
182
- set_log_level
183
-
184
- WorkLogServer.new.start
114
+ app = WorkLogApp.new(@storage)
115
+ WorkLogServer.new(app).start
185
116
  end
186
117
 
187
118
  desc 'stats', 'Show statistics for the work log'
188
119
  def stats
189
- set_log_level
190
-
191
- stats = Statistics.calculate
192
- puts "#{format_left('Total days')}: #{stats.total_days}"
193
- puts "#{format_left('Total entries')}: #{stats.total_entries}"
194
- puts "#{format_left('Total epics')}: #{stats.total_epics}"
195
- puts "#{format_left('Entries per day')}: #{'%.2f' % stats.avg_entries}"
196
- puts "#{format_left('First entry')}: #{stats.first_entry}"
197
- puts "#{format_left('Last entry')}: #{stats.last_entry}"
120
+ worklog = Worklog.new
121
+ worklog.stats(options)
198
122
  end
199
123
 
200
124
  desc 'summary', 'Generate a summary of the work log entries'
@@ -209,23 +133,12 @@ class WorklogCLI < Thor
209
133
  'Number of days to show starting from --date. Takes precedence over --from and --to if defined.'
210
134
  EOF
211
135
  def summary
212
- set_log_level
213
-
214
- start_date, end_date = start_end_date(options)
215
- entries = Storage.days_between(start_date, end_date).map(&:entries).flatten
216
-
217
- # Do nothing if no entries are found.
218
- if entries.empty?
219
- Printer.no_entries(start_date, end_date)
220
- return
221
- end
222
- puts Summary.generate_summary(entries)
136
+ worklog = Worklog.new
137
+ worklog.summary(options)
223
138
  end
224
139
 
225
140
  desc 'version', 'Show the version of the Worklog'
226
141
  def version
227
- set_log_level
228
-
229
142
  puts "Worklog #{current_version} running on Ruby #{RUBY_VERSION}"
230
143
  end
231
144
 
@@ -233,40 +146,4 @@ class WorklogCLI < Thor
233
146
  map 'a' => :add
234
147
  map 'statistics' => :stats
235
148
  map 'serve' => :server
236
-
237
- no_commands do
238
- def set_log_level
239
- # Set the log level based on the verbose option
240
- WorkLogger.level = options[:verbose] ? Logger::Severity::DEBUG : Logger::Severity::INFO
241
- end
242
-
243
- def format_left(string)
244
- # Format a string to be left-aligned in a fixed-width field
245
- #
246
- # @param string [String] the string to format
247
- # @return [String] the formatted string
248
- format('%18s', string)
249
- end
250
-
251
- # Parse the start and end date based on the options provided
252
- #
253
- # @param options [Hash] the options hash
254
- # @return [Array] the start and end date as an array
255
- def start_end_date(options)
256
- if options[:days]
257
- # Safeguard against negative days
258
- raise ArgumentError, 'Number of days cannot be negative' if options[:days].negative?
259
-
260
- start_date = Date.today - options[:days]
261
- end_date = Date.today
262
- elsif options[:from]
263
- start_date = DateParser.parse_date_string!(options[:from], true)
264
- end_date = DateParser.parse_date_string!(options[:to], false) if options[:to]
265
- else
266
- start_date = Date.strptime(options[:date], '%Y-%m-%d')
267
- end_date = start_date
268
- end
269
- [start_date, end_date]
270
- end
271
- end
272
149
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ # Configuration class for the application.
6
+ class Configuration
7
+ attr_accessor :storage_path, :log_level, :webserver_port
8
+
9
+ def initialize(&block)
10
+ block.call(self) if block_given?
11
+
12
+ # Set default values if not set
13
+ @storage_path ||= File.join(Dir.home, '.worklog')
14
+ @log_level ||= :info
15
+ @webserver_port ||= 3000
16
+ end
17
+ end
18
+
19
+ def load_configuration
20
+ # TODO: Implement loading configuration from a file
21
+ file_path = File.join(Dir.home, '.worklog.yaml')
22
+ if File.exist?(file_path)
23
+ file_cfg = YAML.load_file(file_path)
24
+ Configuration.new do |cfg|
25
+ cfg.storage_path = file_cfg['storage_path'] if file_cfg['storage_path']
26
+ cfg.log_level = file_cfg['log_level'].to_sym if file_cfg['log_level']
27
+ cfg.webserver_port = file_cfg['webserver_port'] if file_cfg['webserver_port']
28
+ end
29
+ else
30
+ puts "Configuration file does not exist in #{file_path}"
31
+ end
32
+ end
data/worklog/log_entry.rb CHANGED
@@ -7,6 +7,8 @@ require_relative 'hash'
7
7
 
8
8
  # A single log entry.
9
9
  class LogEntry
10
+ PERSON_REGEX = /\s[~@](\w+)/
11
+
10
12
  include Hashify
11
13
 
12
14
  # Represents a single entry in the work log.
@@ -29,13 +31,27 @@ class LogEntry
29
31
  end
30
32
 
31
33
  # Returns the message string with formatting without the time.
32
- def message_string
34
+ # @param people Hash[String, Person] A hash of people with their handles as keys.
35
+ def message_string(known_people = nil)
36
+ # replace all mentions of people with their names.
37
+ msg = @message.dup
38
+ people.each do |person|
39
+ next unless known_people && known_people[person]
40
+
41
+ msg.gsub!(PERSON_REGEX) do |match|
42
+ s = ''
43
+ s += ' ' if match[0] == ' '
44
+ s += "#{Rainbow(known_people[person].name).underline} (~#{person})" if known_people && known_people[person]
45
+ s
46
+ end
47
+ end
48
+
33
49
  s = ''
34
50
 
35
51
  s += if epic
36
- Rainbow("[EPIC] #{@message}").bg(:white).fg(:black)
52
+ Rainbow("[EPIC] #{msg}").bg(:white).fg(:black)
37
53
  else
38
- message
54
+ msg
39
55
  end
40
56
 
41
57
  s += " [#{Rainbow(@ticket).fg(:blue)}]" if @ticket
@@ -57,7 +73,7 @@ class LogEntry
57
73
  # Empty array if no people are mentioned.
58
74
  #
59
75
  # @return [Array<String>]
60
- @message.scan(/\s[~@](\w+)/).flatten.uniq.sort
76
+ @message.scan(PERSON_REGEX).flatten.uniq.sort
61
77
  end
62
78
 
63
79
  def people?
data/worklog/printer.rb CHANGED
@@ -3,11 +3,17 @@
3
3
  require 'rainbow'
4
4
 
5
5
  # Printer for work log entries
6
- module Printer
6
+ class Printer
7
+ attr_reader :people
8
+
9
+ def initialize(people = nil)
10
+ @people = (people || []).to_h { |person| [person.handle, person] }
11
+ end
12
+
7
13
  # Prints a whole day of work log entries.
8
14
  # If date_inline is true, the date is printed inline with the time.
9
15
  # If epics_only is true, only epic entries are printed.
10
- def self.print_day(daily_log, date_inline = false, epics_only = false)
16
+ def print_day(daily_log, date_inline = false, epics_only = false)
11
17
  daily_log.date = Date.strptime(daily_log.date, '%Y-%m-%d') unless daily_log.date.respond_to?(:strftime)
12
18
 
13
19
  date_string = daily_log.date.strftime('%a, %B %-d, %Y')
@@ -24,7 +30,7 @@ module Printer
24
30
  # @param start_date [Date]
25
31
  # @param end_date [Date]
26
32
  # @return [void]
27
- def self.no_entries(start_date, end_date)
33
+ def no_entries(start_date, end_date)
28
34
  if start_date == end_date
29
35
  date_string = start_date.strftime('%a, %B %-d, %Y')
30
36
  puts "No entries found for #{Rainbow(date_string).gold}."
@@ -35,9 +41,10 @@ module Printer
35
41
  end
36
42
  end
37
43
 
38
- private
39
-
40
44
  # Prints a single entry, formats the date and time.
45
+ # @param daily_log [DailyLog]
46
+ # @param entry [LogEntry]
47
+ # @param date_inline [Boolean] If true, the date is printed inline with the time.
41
48
  def print_entry(daily_log, entry, date_inline = false)
42
49
  entry.time = DateTime.strptime(entry.time, '%H:%M:%S') unless entry.time.respond_to?(:strftime)
43
50
 
@@ -47,8 +54,6 @@ module Printer
47
54
  entry.time.strftime('%H:%M')
48
55
  end
49
56
 
50
- puts "#{Rainbow(time_string).gold} #{entry.message_string}"
57
+ puts "#{Rainbow(time_string).gold} #{entry.message_string(@people)}"
51
58
  end
52
-
53
- module_function :print_entry
54
59
  end
@@ -6,11 +6,17 @@ require_relative 'storage'
6
6
  STATS = Data.define(:total_days, :total_entries, :total_epics, :avg_entries, :first_entry, :last_entry)
7
7
 
8
8
  # Module for calculating statistics for the work log.
9
- module Statistics
9
+ class Statistics
10
+ # Initialize the Statistics class.
11
+ def initialize(config)
12
+ @config = config
13
+ @storage = Storage.new(config)
14
+ end
15
+
10
16
  # Calculate statistics for the work log for all days.
11
17
  # @return [STATS] The statistics for the work log
12
- def self.calculate
13
- all_entries = Storage.all_days
18
+ def calculate
19
+ all_entries = @storage.all_days
14
20
  return STATS.new(0, 0, 0, 0, Date.today, Date.today) if all_entries.empty?
15
21
 
16
22
  total_days = all_entries.length
data/worklog/storage.rb CHANGED
@@ -2,27 +2,32 @@
2
2
 
3
3
  require 'rainbow'
4
4
  require_relative 'daily_log'
5
+ require_relative 'log_entry'
5
6
  require_relative 'logger'
6
7
  require_relative 'person'
7
8
 
8
- module Storage
9
+ class Storage
9
10
  # LogNotFoundError is raised when a log file is not found
10
11
  class LogNotFoundError < StandardError; end
11
12
 
12
13
  FILE_SUFFIX = '.yaml'
13
14
  DATA_DIR = File.join(Dir.home, '.worklog')
14
15
 
15
- def self.folder_exists?
16
- Dir.exist?(DATA_DIR)
16
+ def initialize(config)
17
+ @config = config
18
+ end
19
+
20
+ def folder_exists?
21
+ Dir.exist?(@config.storage_path)
17
22
  end
18
23
 
19
24
  # Return all days with logs
20
25
  # @return [Array<DailyLog>] List of logs
21
- def self.all_days
26
+ def all_days
22
27
  return [] unless folder_exists?
23
28
 
24
29
  logs = []
25
- Dir.glob(File.join(DATA_DIR, "*#{FILE_SUFFIX}")).map do |file|
30
+ Dir.glob(File.join(@config.storage_path, "*#{FILE_SUFFIX}")).map do |file|
26
31
  next if file.end_with?('people.yaml')
27
32
 
28
33
  logs << load_log(file)
@@ -37,8 +42,8 @@ module Storage
37
42
  # @param [Date] start_date The start date, inclusive
38
43
  # @param [Date] end_date The end date, inclusive
39
44
  # @param [Boolean] epics_only If true, only return logs with epic entries
40
- # @param [Array] tags_filter If provided, only return logs with entries that have at least one of the tags
41
- def self.days_between(start_date, end_date = nil, epics_only = nil, tags_filter = nil)
45
+ # @param [Array<String>] tags_filter If provided, only return logs with entries that have at least one of the tags
46
+ def days_between(start_date, end_date = nil, epics_only = nil, tags_filter = nil)
42
47
  return [] unless folder_exists?
43
48
 
44
49
  logs = []
@@ -66,20 +71,20 @@ module Storage
66
71
 
67
72
  # Create file for a new day if it does not exist
68
73
  # @param [Date] date The date, used as the file name.
69
- def self.create_file_skeleton(date)
74
+ def create_file_skeleton(date)
70
75
  create_folder
71
76
 
72
77
  File.write(filepath(date), YAML.dump(DailyLog.new(date:, entries: []))) unless File.exist?(filepath(date))
73
78
  end
74
79
 
75
- def self.load_log(file)
80
+ def load_log(file)
76
81
  load_log!(file)
77
82
  rescue LogNotFoundError
78
83
  WorkLogger.error "No work log found for #{file}. Aborting."
79
84
  nil
80
85
  end
81
86
 
82
- def self.load_log!(file)
87
+ def load_log!(file)
83
88
  WorkLogger.debug "Loading file #{file}"
84
89
  begin
85
90
  log = YAML.load_file(file, permitted_classes: [Date, Time, DailyLog, LogEntry])
@@ -92,7 +97,7 @@ module Storage
92
97
  end
93
98
  end
94
99
 
95
- def self.write_log(file, daily_log)
100
+ def write_log(file, daily_log)
96
101
  create_folder
97
102
 
98
103
  WorkLogger.debug "Writing to file #{file}"
@@ -102,7 +107,7 @@ module Storage
102
107
  end
103
108
  end
104
109
 
105
- def self.load_single_log_file(file, headline = true)
110
+ def load_single_log_file(file, headline = true)
106
111
  daily_log = load_log!(file)
107
112
  puts "Work log for #{Rainbow(daily_log.date).gold}:" if headline
108
113
  daily_log.entries
@@ -110,7 +115,7 @@ module Storage
110
115
 
111
116
  # Load all people from the people file
112
117
  # @return [Array<Person>] List of people
113
- def self.load_people!
118
+ def load_people!
114
119
  people_file = File.join(DATA_DIR, 'people.yaml')
115
120
  return [] unless File.exist?(people_file)
116
121
 
@@ -119,28 +124,24 @@ module Storage
119
124
 
120
125
  # Write people to the people file
121
126
  # @param [Array<Person>] people List of people
122
- def self.write_people!(people)
127
+ def write_people!(people)
123
128
  create_folder
124
129
 
125
- people_file = File.join(DATA_DIR, 'people.yaml')
130
+ people_file = File.join(@config.storage_path, 'people.yaml')
126
131
  File.open(people_file, 'w') do |f|
127
132
  f.puts people.to_yaml
128
133
  end
129
134
  end
130
135
 
131
- private
132
-
133
136
  # Create folder if not exists already.
134
137
  def create_folder
135
- Dir.mkdir(DATA_DIR) unless Dir.exist?(DATA_DIR)
138
+ Dir.mkdir(@config.storage_path) unless Dir.exist?(@config.storage_path)
136
139
  end
137
140
 
138
141
  # Construct filepath for a given date.
139
142
  # @param [Date] date The date
140
143
  # @return [String] The filepath
141
144
  def filepath(date)
142
- File.join(DATA_DIR, "#{date}#{FILE_SUFFIX}")
145
+ File.join(@config.storage_path, "#{date}#{FILE_SUFFIX}")
143
146
  end
144
-
145
- module_function :create_folder, :filepath
146
147
  end
@@ -24,4 +24,12 @@ module StringHelper
24
24
  "#{singular}s"
25
25
  end
26
26
  end
27
+
28
+ # Format a string to be left-aligned in a fixed-width field
29
+ #
30
+ # @param string [String] the string to format
31
+ # @return [String] the formatted string
32
+ def format_left(string)
33
+ format('%18s', string)
34
+ end
27
35
  end
data/worklog/summary.rb CHANGED
@@ -47,6 +47,8 @@ module Summary
47
47
  end
48
48
 
49
49
  # Generate a summary from provided log entries.
50
+ # @param log_entries [Array<LogEntry>] The log entries to summarize.
51
+ # @return [String] The generated summary.
50
52
  def self.generate_summary(log_entries)
51
53
  prompt = build_prompt(log_entries)
52
54
 
data/worklog/webserver.rb CHANGED
@@ -24,8 +24,11 @@ class DefaultHeaderMiddleware
24
24
  end
25
25
  end
26
26
 
27
+ # Class to render the main page of the WorkLog web application.
27
28
  class WorkLogResponse
28
- # Class to render the main page of the WorkLog web application.
29
+ def initialize(storage)
30
+ @storage = storage
31
+ end
29
32
 
30
33
  def response(request)
31
34
  template = ERB.new(File.read(File.join(File.dirname(__FILE__), 'templates', 'index.html.erb')), trim_mode: '-')
@@ -34,7 +37,7 @@ class WorkLogResponse
34
37
  tags = @params['tags'].nil? ? nil : @params['tags'].split(',')
35
38
  epics_only = @params['epics_only'] == 'true'
36
39
  presentation = @params['presentation'] == 'true'
37
- logs = Storage.days_between(Date.today - days, Date.today, epics_only, tags).reverse
40
+ logs = @storage.days_between(Date.today - days, Date.today, epics_only, tags).reverse
38
41
  total_entries = logs.sum { |entry| entry.entries.length }
39
42
  _ = total_entries
40
43
  _ = presentation
@@ -62,9 +65,13 @@ class WorkLogResponse
62
65
  end
63
66
 
64
67
  class WorkLogApp
65
- def self.call(env)
68
+ def initialize(storage)
69
+ @storage = storage
70
+ end
71
+
72
+ def call(env)
66
73
  req = Rack::Request.new(env)
67
- WorkLogResponse.new.response(req)
74
+ WorkLogResponse.new(@storage).response(req)
68
75
  end
69
76
  end
70
77
 
@@ -79,8 +86,12 @@ end
79
86
 
80
87
  class WorkLogServer
81
88
  # Main Rack server containing all endpoints.
89
+ def initialize(worklog_app)
90
+ @worklog_app = worklog_app
91
+ end
82
92
 
83
93
  def start
94
+ worklog_app = @worklog_app
84
95
  app = Rack::Builder.new do
85
96
  use Rack::Deflater
86
97
  use Rack::CommonLogger
@@ -89,7 +100,7 @@ class WorkLogServer
89
100
  use DefaultHeaderMiddleware
90
101
 
91
102
  map '/' do
92
- run WorkLogApp
103
+ run worklog_app
93
104
  end
94
105
  # TODO: Future development
95
106
  # map '/favicon.svg' do
data/worklog/worklog.rb CHANGED
@@ -7,8 +7,168 @@ require 'yaml'
7
7
 
8
8
  require_relative 'hash'
9
9
  require_relative 'daily_log'
10
+ require_relative 'date_parser'
10
11
  require_relative 'log_entry'
11
12
  require_relative 'storage'
13
+ require_relative 'logger'
14
+ require_relative 'string_helper'
15
+ require_relative 'printer'
16
+ require_relative 'statistics'
12
17
 
13
- module Worklog
18
+ # Main class providing all worklog functionality.
19
+ # This class is the main entry point for the application.
20
+ # It handles command line arguments, configuration, and logging.
21
+ class Worklog
22
+ include StringHelper
23
+ attr_reader :config
24
+
25
+ def initialize(config = nil)
26
+ @config = config || Configuration.new
27
+ @storage = Storage.new(@config)
28
+
29
+ WorkLogger.level = @config.log_level == :debug ? Logger::Severity::DEBUG : Logger::Severity::INFO
30
+ end
31
+
32
+ def add(message, options = {})
33
+ # Remove leading and trailing whitespaces
34
+ # Raise an error if the message is empty
35
+ message = message.strip
36
+ raise ArgumentError, 'Message cannot be empty' if message.empty?
37
+
38
+ date = Date.strptime(options[:date], '%Y-%m-%d')
39
+ time = Time.strptime(options[:time], '%H:%M:%S')
40
+ @storage.create_file_skeleton(date)
41
+
42
+ daily_log = @storage.load_log!(@storage.filepath(date))
43
+ daily_log.entries << LogEntry.new(time:, tags: options[:tags], ticket: options[:ticket], url: options[:url],
44
+ epic: options[:epic], message:)
45
+
46
+ # Sort by time in case an entry was added later out of order.
47
+ daily_log.entries.sort_by!(&:time)
48
+
49
+ @storage.write_log(@storage.filepath(options[:date]), daily_log)
50
+
51
+ WorkLogger.info Rainbow("Added entry on #{options[:date]}: #{message}").green
52
+ end
53
+
54
+ def edit(options = {})
55
+ date = Date.strptime(options[:date], '%Y-%m-%d')
56
+
57
+ # Load existing log
58
+ log = @storage.load_log(@storage.filepath(date))
59
+ unless log
60
+ WorkLogger.error "No work log found for #{options[:date]}. Aborting."
61
+ exit 1
62
+ end
63
+
64
+ txt = Editor::EDITOR_PREAMBLE.result_with_hash(content: YAML.dump(log))
65
+ return_val = Editor.open_editor(txt)
66
+
67
+ @storage.write_log(@storage.filepath(date),
68
+ YAML.load(return_val, permitted_classes: [Date, Time, DailyLog, LogEntry]))
69
+ WorkLogger.info Rainbow("Updated work log for #{options[:date]}").green
70
+ end
71
+
72
+ def show(options = {})
73
+ people = @storage.load_people!
74
+ printer = Printer.new(people)
75
+
76
+ start_date, end_date = start_end_date(options)
77
+
78
+ entries = @storage.days_between(start_date, end_date)
79
+ if entries.empty?
80
+ printer.no_entries(start_date, end_date)
81
+ else
82
+ entries.each do |entry|
83
+ printer.print_day(entry, entries.size > 1, options[:epics_only])
84
+ end
85
+ end
86
+ end
87
+
88
+ def people(_options = {})
89
+ puts 'People mentioned in the work log:'
90
+
91
+ mentions = {}
92
+ all_logs = @storage.all_days
93
+ all_logs.map(&:people).each do |people|
94
+ mentions.merge!(people) { |_key, oldval, newval| oldval + newval }
95
+ end
96
+ mentions.each { |k, v| puts "#{Rainbow(k).gold}: #{v} #{pluralize(v, 'occurrence')}" }
97
+ end
98
+
99
+ def tags(_options = {})
100
+ all_logs = @storage.all_days
101
+
102
+ puts Rainbow('Tags used in the work log:').gold
103
+
104
+ # Count all tags used in the work log
105
+ tags = all_logs.map(&:entries).flatten.map(&:tags).flatten.compact.tally
106
+
107
+ # Determine length of longest tag for formatting
108
+ # Add one additonal space for formatting
109
+ max_len = tags.empty? ? 0 : tags.keys.map(&:length).max + 1
110
+
111
+ tags.sort.each { |k, v| puts "#{Rainbow(k.ljust(max_len)).gold}: #{v} #{pluralize(v, 'occurrence')}" }
112
+ end
113
+
114
+ def stats(_options = {})
115
+ stats = Statistics.new(@config).calculate
116
+ puts "#{format_left('Total days')}: #{stats.total_days}"
117
+ puts "#{format_left('Total entries')}: #{stats.total_entries}"
118
+ puts "#{format_left('Total epics')}: #{stats.total_epics}"
119
+ puts "#{format_left('Entries per day')}: #{format('%.2f', stats.avg_entries)}"
120
+ puts "#{format_left('First entry')}: #{stats.first_entry}"
121
+ puts "#{format_left('Last entry')}: #{stats.last_entry}"
122
+ end
123
+
124
+ def summary(options = {})
125
+ start_date, end_date = start_end_date(options)
126
+ entries = @storage.days_between(start_date, end_date).map(&:entries).flatten
127
+
128
+ # Do nothing if no entries are found.
129
+ if entries.empty?
130
+ Printer.new.no_entries(start_date, end_date)
131
+ return
132
+ end
133
+ puts Summary.generate_summary(entries)
134
+ end
135
+
136
+ def remove(options = {})
137
+ date = Date.strptime(options[:date], '%Y-%m-%d')
138
+ unless File.exist?(@storage.filepath(date))
139
+ WorkLogger.error Rainbow("No work log found for #{options[:date]}. Aborting.").red
140
+ exit 1
141
+ end
142
+
143
+ daily_log = @storage.load_log!(@storage.filepath(options[:date]))
144
+ if daily_log.entries.empty?
145
+ WorkLogger.error Rainbow("No entries found for #{options[:date]}. Aborting.").red
146
+ exit 1
147
+ end
148
+
149
+ removed_entry = daily_log.entries.pop
150
+ @storage.write_log(@storage.filepath(date), daily_log)
151
+ WorkLogger.info Rainbow("Removed entry: #{removed_entry.message}").green
152
+ end
153
+
154
+ # Parse the start and end date based on the options provided
155
+ #
156
+ # @param options [Hash] the options hash
157
+ # @return [Array] the start and end date as an array
158
+ def start_end_date(options)
159
+ if options[:days]
160
+ # Safeguard against negative days
161
+ raise ArgumentError, 'Number of days cannot be negative' if options[:days].negative?
162
+
163
+ start_date = Date.today - options[:days]
164
+ end_date = Date.today
165
+ elsif options[:from]
166
+ start_date = DateParser.parse_date_string!(options[:from], true)
167
+ end_date = DateParser.parse_date_string!(options[:to], false) if options[:to]
168
+ else
169
+ start_date = Date.strptime(options[:date], '%Y-%m-%d')
170
+ end_date = start_date
171
+ end
172
+ [start_date, end_date]
173
+ end
14
174
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fewald-worklog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.11
4
+ version: 0.1.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Friedrich Ewald
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-04-03 00:00:00.000000000 Z
10
+ date: 2025-04-15 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: httparty
@@ -109,6 +109,7 @@ files:
109
109
  - ".version"
110
110
  - bin/wl
111
111
  - worklog/cli.rb
112
+ - worklog/configuration.rb
112
113
  - worklog/daily_log.rb
113
114
  - worklog/date_parser.rb
114
115
  - worklog/editor.rb