fewald-worklog 0 → 0.1.1

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.
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ # Returns the current version of the gem from `.version`.
6
+ # Versioning follows SemVer.
7
+ # @return [String] The current version of the gem.
8
+ def current_version
9
+ version_file_path = File.join(Pathname.new(__dir__).parent, '.version')
10
+ File.read(version_file_path).strip
11
+ end
@@ -6,8 +6,8 @@ require 'rack'
6
6
  require 'rack/constants'
7
7
  require 'rackup'
8
8
  require 'uri'
9
- require 'storage'
10
- require 'worklog'
9
+ require_relative 'storage'
10
+ require_relative 'worklog'
11
11
 
12
12
  class DefaultHeaderMiddleware
13
13
  # Rack middleware to add default headers to the response.
@@ -24,12 +24,8 @@ class DefaultHeaderMiddleware
24
24
  end
25
25
  end
26
26
 
27
- # Class to render the main page of the WorkLog web application.
28
27
  class WorkLogResponse
29
- def initialize(storage, tags)
30
- @storage = storage
31
- @tags = tags
32
- end
28
+ # Class to render the main page of the WorkLog web application.
33
29
 
34
30
  def response(request)
35
31
  template = ERB.new(File.read(File.join(File.dirname(__FILE__), 'templates', 'index.html.erb')), trim_mode: '-')
@@ -38,7 +34,7 @@ class WorkLogResponse
38
34
  tags = @params['tags'].nil? ? nil : @params['tags'].split(',')
39
35
  epics_only = @params['epics_only'] == 'true'
40
36
  presentation = @params['presentation'] == 'true'
41
- logs = @storage.days_between(Date.today - days, Date.today, epics_only, tags).reverse
37
+ logs = Storage.days_between(Date.today - days, Date.today, epics_only, tags).reverse
42
38
  total_entries = logs.sum { |entry| entry.entries.length }
43
39
  _ = total_entries
44
40
  _ = presentation
@@ -66,14 +62,9 @@ class WorkLogResponse
66
62
  end
67
63
 
68
64
  class WorkLogApp
69
- def initialize(storage)
70
- @storage = storage
71
- @tags = @storage.tags
72
- end
73
-
74
- def call(env)
65
+ def self.call(env)
75
66
  req = Rack::Request.new(env)
76
- WorkLogResponse.new(@storage, @tags).response(req)
67
+ WorkLogResponse.new.response(req)
77
68
  end
78
69
  end
79
70
 
@@ -88,12 +79,8 @@ end
88
79
 
89
80
  class WorkLogServer
90
81
  # Main Rack server containing all endpoints.
91
- def initialize(worklog_app)
92
- @worklog_app = worklog_app
93
- end
94
82
 
95
83
  def start
96
- worklog_app = @worklog_app
97
84
  app = Rack::Builder.new do
98
85
  use Rack::Deflater
99
86
  use Rack::CommonLogger
@@ -102,7 +89,7 @@ class WorkLogServer
102
89
  use DefaultHeaderMiddleware
103
90
 
104
91
  map '/' do
105
- run worklog_app
92
+ run WorkLogApp
106
93
  end
107
94
  # TODO: Future development
108
95
  # map '/favicon.svg' do
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'hash'
5
+ require_relative 'daily_log'
6
+ require_relative 'log_entry'
7
+ require_relative 'storage'
8
+
9
+ require 'optparse'
10
+ require 'rainbow'
11
+ require 'yaml'
12
+
13
+ module Worklog
14
+ end
metadata CHANGED
@@ -1,112 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fewald-worklog
3
3
  version: !ruby/object:Gem::Version
4
- version: '0'
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Friedrich Ewald
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: httparty
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: 0.22.0
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: 0.22.0
26
- - !ruby/object:Gem::Dependency
27
- name: logger
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '1.6'
33
- type: :runtime
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '1.6'
40
- - !ruby/object:Gem::Dependency
41
- name: puma
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - "~>"
45
- - !ruby/object:Gem::Version
46
- version: '6.6'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '6.6'
54
- - !ruby/object:Gem::Dependency
55
- name: rack
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '3.1'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - "~>"
66
- - !ruby/object:Gem::Version
67
- version: '3.1'
68
- - !ruby/object:Gem::Dependency
69
- name: rackup
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '2.2'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '2.2'
82
- - !ruby/object:Gem::Dependency
83
- name: rainbow
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: '3.1'
89
- type: :runtime
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - "~>"
94
- - !ruby/object:Gem::Version
95
- version: '3.1'
96
- - !ruby/object:Gem::Dependency
97
- name: thor
98
- requirement: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - "~>"
101
- - !ruby/object:Gem::Version
102
- version: '1.3'
103
- type: :runtime
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '1.3'
10
+ date: 2025-03-12 00:00:00.000000000 Z
11
+ dependencies: []
110
12
  description: |
111
13
  Command line tool for tracking achievments, tasks and interactions.
112
14
 
@@ -120,27 +22,25 @@ executables:
120
22
  extensions: []
121
23
  extra_rdoc_files: []
122
24
  files:
123
- - ".version"
124
25
  - bin/wl
125
- - lib/cli.rb
126
- - lib/configuration.rb
127
- - lib/daily_log.rb
128
- - lib/date_parser.rb
129
- - lib/editor.rb
130
- - lib/hash.rb
131
- - lib/log_entry.rb
132
- - lib/person.rb
133
- - lib/printer.rb
134
- - lib/statistics.rb
135
- - lib/storage.rb
136
- - lib/string_helper.rb
137
- - lib/summary.rb
138
- - lib/templates/favicon.svg.erb
139
- - lib/templates/index.html.erb
140
- - lib/version.rb
141
- - lib/webserver.rb
142
- - lib/worklog.rb
143
- - lib/worklogger.rb
26
+ - worklog/cli.rb
27
+ - worklog/daily_log.rb
28
+ - worklog/date_parser.rb
29
+ - worklog/editor.rb
30
+ - worklog/hash.rb
31
+ - worklog/log_entry.rb
32
+ - worklog/logger.rb
33
+ - worklog/person.rb
34
+ - worklog/printer.rb
35
+ - worklog/statistics.rb
36
+ - worklog/storage.rb
37
+ - worklog/string_helper.rb
38
+ - worklog/summary.rb
39
+ - worklog/templates/favicon.svg.erb
40
+ - worklog/templates/index.html.erb
41
+ - worklog/version.rb
42
+ - worklog/webserver.rb
43
+ - worklog/worklog.rb
144
44
  homepage: https://github.com/f-ewald/worklog
145
45
  licenses:
146
46
  - MIT
@@ -165,7 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
165
65
  - !ruby/object:Gem::Version
166
66
  version: '0'
167
67
  requirements: []
168
- rubygems_version: 3.6.7
68
+ rubygems_version: 3.6.2
169
69
  specification_version: 4
170
70
  summary: Command line tool for tracking achievments, tasks and interactions.
171
71
  test_files: []
data/.version DELETED
@@ -1 +0,0 @@
1
-
data/lib/cli.rb DELETED
@@ -1,149 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Add the current directory to the load path
4
- # curr_dir = File.expand_path(__dir__)
5
- # $LOAD_PATH << curr_dir unless $LOAD_PATH.include?(curr_dir)
6
-
7
- require 'thor'
8
- require 'date'
9
- require 'worklogger'
10
-
11
- require 'worklog'
12
- require 'date_parser'
13
- require 'configuration'
14
- require 'editor'
15
- require 'printer'
16
- require 'statistics'
17
- require 'storage'
18
- require 'string_helper'
19
- require 'summary'
20
- require 'version'
21
- require 'webserver'
22
-
23
- # CLI for the work log application
24
- class WorklogCLI < Thor
25
- attr_accessor :config, :storage
26
-
27
- include StringHelper
28
- class_option :verbose, type: :boolean, aliases: '-v', desc: 'Enable verbose output'
29
-
30
- package_name 'Worklog'
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
-
39
- def self.exit_on_failure?
40
- true
41
- end
42
-
43
- desc 'add MESSAGE', 'Add a new entry to the work log, defaults to the current date.'
44
- long_desc <<~LONGDESC
45
- Add a new entry with the current date and time to the work log.
46
- The message is required and must be enclosed in quotes if it contains more than one word.
47
-
48
- People can be referenced either by using the tilde "~" or the at symbol "@", followed by
49
- an alphanumeric string.
50
- LONGDESC
51
- option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d'), desc: 'Set the date of the entry'
52
- option :time, type: :string, default: DateTime.now.strftime('%H:%M:%S'), desc: 'Set the time of the entry'
53
- option :tags, type: :array, default: [], desc: 'Add tags to the entry'
54
- option :ticket, type: :string, desc: 'Ticket number associated with the entry. Can be any alphanumeric string.'
55
- option :url, type: :string, desc: 'URL to associate with the entry'
56
- option :epic, type: :boolean, default: false, desc: 'Mark the entry as an epic'
57
- def add(message)
58
- worklog = Worklog.new
59
- worklog.add(message, options)
60
- end
61
-
62
- desc 'edit', 'Edit a day in the work log. By default, the current date is used.'
63
- option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
64
- def edit
65
- worklog = Worklog.new
66
- worklog.edit(options)
67
- end
68
-
69
- desc 'remove', 'Remove last entry from the log'
70
- option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
71
- def remove
72
- worklog = Worklog.new
73
- worklog.remove(options)
74
- end
75
-
76
- desc 'show', 'Show the work log for a specific date or a range of dates. Defaults to todays date.'
77
- long_desc <<~LONGDESC
78
- Show the work log for a specific date or a range of dates. As a default, all items from the current day will be shown.
79
- LONGDESC
80
- option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d'),
81
- desc: <<~DESC
82
- Show the work log for a specific date. If this option is provided, --from and --to and --days should not be used.
83
- DESC
84
- option :from, type: :string, desc: <<~EOF
85
- Inclusive start date of the range. Takes precedence over --date, if defined.
86
- EOF
87
- option :to, type: :string, desc: <<~EOF
88
- Inclusive end date of the range. Takes precedence over --date, if defined.
89
- EOF
90
- option :days, type: :numeric, desc: <<~EOF
91
- Number of days to show starting from --date. Takes precedence over --from and --to if defined.
92
- EOF
93
- option :epics_only, type: :boolean, default: false, desc: 'Show only entries that are marked as epic'
94
- option :tags, type: :array, default: [], desc: 'Filter entries by tags. Tags are treated as an OR condition.'
95
- def show
96
- worklog = Worklog.new
97
- worklog.show(options)
98
- end
99
-
100
- desc 'people', 'Show all people mentioned in the work log'
101
- def people(person = nil)
102
- worklog = Worklog.new
103
- worklog.people(person, options)
104
- end
105
-
106
- desc 'tags', 'Show all tags used in the work log'
107
- def tags
108
- worklog = Worklog.new
109
- worklog.tags(options)
110
- end
111
-
112
- desc 'server', 'Start the work log server'
113
- def server
114
- app = WorkLogApp.new(@storage)
115
- WorkLogServer.new(app).start
116
- end
117
-
118
- desc 'stats', 'Show statistics for the work log'
119
- def stats
120
- worklog = Worklog.new
121
- worklog.stats(options)
122
- end
123
-
124
- desc 'summary', 'Generate a summary of the work log entries'
125
- option :date, type: :string, default: DateTime.now.strftime('%Y-%m-%d')
126
- option :from, type: :string, desc: <<-EOF
127
- 'Inclusive start date of the range. Takes precedence over --date if defined.'
128
- EOF
129
- option :to, type: :string, desc: <<-EOF
130
- 'Inclusive end date of the range. Takes precedence over --date if defined.'
131
- EOF
132
- option :days, type: :numeric, desc: <<-EOF
133
- 'Number of days to show starting from --date. Takes precedence over --from and --to if defined.'
134
- EOF
135
- def summary
136
- worklog = Worklog.new
137
- worklog.summary(options)
138
- end
139
-
140
- desc 'version', 'Show the version of the Worklog'
141
- def version
142
- puts "Worklog #{current_version} running on Ruby #{RUBY_VERSION}"
143
- end
144
-
145
- # Define shortcuts and aliases
146
- map 'a' => :add
147
- map 'statistics' => :stats
148
- map 'serve' => :server
149
- end
data/lib/configuration.rb DELETED
@@ -1,33 +0,0 @@
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
- # Load configuration from a YAML file.
20
- # The file should be located at ~/.worklog.yaml.
21
- def load_configuration
22
- file_path = File.join(Dir.home, '.worklog.yaml')
23
- if File.exist?(file_path)
24
- file_cfg = YAML.load_file(file_path)
25
- Configuration.new do |cfg|
26
- cfg.storage_path = file_cfg['storage_path'] if file_cfg['storage_path']
27
- cfg.log_level = file_cfg['log_level'].to_sym if file_cfg['log_level']
28
- cfg.webserver_port = file_cfg['webserver_port'] if file_cfg['webserver_port']
29
- end
30
- else
31
- puts "Configuration file does not exist in #{file_path}"
32
- end
33
- end
data/lib/storage.rb DELETED
@@ -1,172 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'rainbow'
4
- require 'daily_log'
5
- require 'log_entry'
6
- require 'worklogger'
7
- require 'person'
8
-
9
- # Handles storage of daily logs and people
10
- class Storage
11
- # LogNotFoundError is raised when a log file is not found
12
- class LogNotFoundError < StandardError; end
13
-
14
- FILE_SUFFIX = '.yaml'
15
-
16
- def initialize(config)
17
- @config = config
18
- end
19
-
20
- def folder_exists?
21
- Dir.exist?(@config.storage_path)
22
- end
23
-
24
- # Return all logs for all available days
25
- # @return [Array<DailyLog>] List of all logs
26
- def all_days
27
- return [] unless folder_exists?
28
-
29
- logs = []
30
- Dir.glob(File.join(@config.storage_path, "*#{FILE_SUFFIX}")).map do |file|
31
- next if file.end_with?('people.yaml')
32
-
33
- logs << load_log(file)
34
- end
35
-
36
- logs
37
- end
38
-
39
- # Return all tags as a set
40
- # @return [Set<String>] Set of all tags
41
- def tags
42
- logs = all_days
43
- tags = Set[]
44
- logs.each do |log|
45
- log.entries.each do |entry|
46
- next unless entry.tags
47
-
48
- entry.tags.each do |tag|
49
- tags << tag
50
- end
51
- end
52
- end
53
- tags
54
- end
55
-
56
- # Return days between start_date and end_date
57
- # If end_date is nil, return logs from start_date to today
58
- #
59
- # @param [Date] start_date The start date, inclusive
60
- # @param [Date] end_date The end date, inclusive
61
- # @param [Boolean] epics_only If true, only return logs with epic entries
62
- # @param [Array<String>] tags_filter If provided, only return logs with entries that have at least one of the tags
63
- # @return [Array<DailyLog>] List of logs
64
- def days_between(start_date, end_date = nil, epics_only = nil, tags_filter = nil)
65
- return [] unless folder_exists?
66
-
67
- logs = []
68
- end_date = Date.today if end_date.nil?
69
-
70
- return [] if start_date > end_date
71
-
72
- while start_date <= end_date
73
- if File.exist?(filepath(start_date))
74
- tmp_logs = load_log!(filepath(start_date))
75
- tmp_logs.entries.keep_if { |entry| entry.epic? } if epics_only
76
-
77
- if tags_filter
78
- # Safeguard against entries without any tags, not just empty array
79
- tmp_logs.entries.keep_if { |entry| entry.tags && (entry.tags & tags_filter).size > 0 }
80
- end
81
-
82
- logs << tmp_logs if tmp_logs.entries.length > 0
83
- end
84
-
85
- start_date += 1
86
- end
87
- logs
88
- end
89
-
90
- # Create file for a new day if it does not exist
91
- # @param [Date] date The date, used as the file name.
92
- def create_file_skeleton(date)
93
- create_folder
94
-
95
- File.write(filepath(date), YAML.dump(DailyLog.new(date:, entries: []))) unless File.exist?(filepath(date))
96
- end
97
-
98
- def load_log(file)
99
- load_log!(file)
100
- rescue LogNotFoundError
101
- WorkLogger.error "No work log found for #{file}. Aborting."
102
- nil
103
- end
104
-
105
- def load_log!(file)
106
- WorkLogger.debug "Loading file #{file}"
107
- begin
108
- log = YAML.load_file(file, permitted_classes: [Date, Time, DailyLog, LogEntry])
109
- log.entries.each do |entry|
110
- entry.time = Time.parse(entry.time) unless entry.time.respond_to?(:strftime)
111
- end
112
- log
113
- rescue Errno::ENOENT
114
- raise LogNotFoundError
115
- end
116
- end
117
-
118
- def write_log(file, daily_log)
119
- create_folder
120
-
121
- WorkLogger.debug "Writing to file #{file}"
122
-
123
- File.open(file, 'w') do |f|
124
- f.puts daily_log.to_yaml
125
- end
126
- end
127
-
128
- def load_single_log_file(file, headline = true)
129
- daily_log = load_log!(file)
130
- puts "Work log for #{Rainbow(daily_log.date).gold}:" if headline
131
- daily_log.entries
132
- end
133
-
134
- def load_people
135
- load_people!
136
- rescue Errno::ENOENT
137
- WorkLogger.info 'Unable to load people.'
138
- []
139
- end
140
-
141
- # Load all people from the people file
142
- # @return [Array<Person>] List of people
143
- def load_people!
144
- people_file = File.join(@config.storage_path, 'people.yaml')
145
- return [] unless File.exist?(people_file)
146
-
147
- YAML.load_file(people_file, permitted_classes: [Person])
148
- end
149
-
150
- # Write people to the people file
151
- # @param [Array<Person>] people List of people
152
- def write_people!(people)
153
- create_folder
154
-
155
- people_file = File.join(@config.storage_path, 'people.yaml')
156
- File.open(people_file, 'w') do |f|
157
- f.puts people.to_yaml
158
- end
159
- end
160
-
161
- # Create folder if not exists already.
162
- def create_folder
163
- Dir.mkdir(@config.storage_path) unless Dir.exist?(@config.storage_path)
164
- end
165
-
166
- # Construct filepath for a given date.
167
- # @param [Date] date The date
168
- # @return [String] The filepath
169
- def filepath(date)
170
- File.join(@config.storage_path, "#{date}#{FILE_SUFFIX}")
171
- end
172
- end
data/lib/version.rb DELETED
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'pathname'
4
-
5
- # Returns the current version of the gem from `.version`.
6
- # Versioning follows SemVer.
7
- # @return [String] The current version of the gem.
8
- def current_version
9
- version_file_path = File.join(Pathname.new(__dir__).parent, '.version')
10
- File.read(version_file_path).strip
11
- end
12
-
13
- # Increment version number according to SemVer.
14
- # @param version [String] The current version.
15
- # @param part [String] The part of the version to increment.
16
- # @return [String] The incremented version.
17
- def increment_version(version, part = 'patch')
18
- major, minor, patch = version.split('.').map(&:to_i)
19
- case part
20
- when 'major'
21
- major += 1
22
- minor = 0
23
- patch = 0
24
- when 'minor'
25
- minor += 1
26
- patch = 0
27
- when 'patch'
28
- patch += 1
29
- end
30
- [major, minor, patch].join('.')
31
- end