dsu 0.1.0.alpha.5 → 1.1.0.alpha.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +6 -15
- data/README.md +33 -47
- data/lib/dsu/base_cli.rb +13 -6
- data/lib/dsu/cli.rb +46 -55
- data/lib/dsu/command_services/add_entry_service.rb +21 -21
- data/lib/dsu/core/ruby/not_today.rb +11 -0
- data/lib/dsu/models/entry.rb +32 -21
- data/lib/dsu/models/entry_group.rb +41 -105
- data/lib/dsu/services/configuration_loader_service.rb +19 -2
- data/lib/dsu/services/entry_group_editor_service.rb +37 -89
- data/lib/dsu/services/stdout_redirector_service.rb +27 -0
- data/lib/dsu/subcommands/list.rb +83 -15
- data/lib/dsu/support/colorable.rb +1 -0
- data/lib/dsu/support/command_options/dsu_times.rb +33 -0
- data/lib/dsu/support/command_options/time.rb +77 -0
- data/lib/dsu/support/command_options/time_mneumonic.rb +127 -0
- data/lib/dsu/support/command_options/time_mneumonics.rb +15 -0
- data/lib/dsu/support/configurable.rb +15 -0
- data/lib/dsu/support/configuration.rb +13 -1
- data/lib/dsu/support/entry_group_fileable.rb +31 -6
- data/lib/dsu/support/entry_group_loadable.rb +13 -16
- data/lib/dsu/support/entry_group_viewable.rb +26 -8
- data/lib/dsu/support/times_sortable.rb +1 -3
- data/lib/dsu/validators/description_validator.rb +38 -0
- data/lib/dsu/validators/entries_validator.rb +43 -32
- data/lib/dsu/validators/time_validator.rb +11 -20
- data/lib/dsu/version.rb +1 -1
- data/lib/dsu/views/edited_entries/shared/errors.rb +39 -0
- data/lib/dsu/views/entry_group/edit.rb +89 -39
- data/lib/dsu/views/entry_group/show.rb +10 -4
- data/lib/dsu/views/shared/messages.rb +56 -0
- data/lib/dsu.rb +8 -2
- metadata +24 -12
- data/lib/dsu/support/commander/command.rb +0 -130
- data/lib/dsu/support/commander/command_help.rb +0 -62
- data/lib/dsu/support/commander/subcommand.rb +0 -45
- data/lib/dsu/support/interactive/cli.rb +0 -161
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'active_model'
|
4
4
|
require_relative '../services/entry_group_editor_service'
|
5
5
|
require_relative '../services/entry_group_deleter_service'
|
6
6
|
require_relative '../services/entry_group_reader_service'
|
@@ -13,30 +13,28 @@ require_relative 'entry'
|
|
13
13
|
|
14
14
|
module Dsu
|
15
15
|
module Models
|
16
|
-
class
|
16
|
+
# This class represents a group of entries for a given day. IOW,
|
17
|
+
# things someone might want to share at their daily standup (DSU).
|
18
|
+
class EntryGroup
|
19
|
+
include ActiveModel::Model
|
17
20
|
extend Support::EntryGroupLoadable
|
18
21
|
include Support::TimeFormatable
|
19
22
|
|
20
|
-
|
21
|
-
|
23
|
+
attr_accessor :time
|
24
|
+
attr_reader :entries
|
25
|
+
|
26
|
+
validates_with Validators::EntriesValidator
|
27
|
+
validates_with Validators::TimeValidator
|
22
28
|
|
23
29
|
def initialize(time: nil, entries: [])
|
24
30
|
raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
|
25
|
-
raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array) || entries.nil?
|
26
|
-
|
27
|
-
time ||= Time.now
|
28
|
-
time = time.localtime if time.utc?
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
super(hash: {
|
33
|
-
time: time,
|
34
|
-
entries: entries
|
35
|
-
})
|
32
|
+
@time = ensure_local_time(time)
|
33
|
+
self.entries = entries || []
|
36
34
|
end
|
37
35
|
|
38
36
|
class << self
|
39
|
-
def delete(time:, options: {})
|
37
|
+
def delete!(time:, options: {})
|
40
38
|
Services::EntryGroupDeleterService.new(time: time, options: options).call
|
41
39
|
end
|
42
40
|
|
@@ -46,127 +44,65 @@ module Dsu
|
|
46
44
|
# return new(time: time) unless exists?(time: time)
|
47
45
|
|
48
46
|
load(time: time).tap do |entry_group|
|
49
|
-
|
47
|
+
Services::EntryGroupEditorService.new(entry_group: entry_group, options: options).call
|
50
48
|
end
|
51
49
|
end
|
52
50
|
|
53
51
|
def exists?(time:)
|
54
52
|
Dsu::Services::EntryGroupReaderService.entry_group_file_exists?(time: time)
|
55
53
|
end
|
54
|
+
end
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
new(**hydrated_entry_group_hash_for(time: time))
|
61
|
-
end
|
62
|
-
|
63
|
-
# This function returns a hash whose :time and :entries
|
64
|
-
# key values are hydrated with instantiated Time and Entry
|
65
|
-
# objects.
|
66
|
-
def hydrated_entry_group_hash_for(time:)
|
67
|
-
entry_group_hash = entry_group_hash_for(time: time)
|
68
|
-
hydrate_entry_group_hash(entry_group_hash: entry_group_hash, time: time)
|
69
|
-
end
|
56
|
+
def valid_unique_entries
|
57
|
+
entries&.select(&:valid?)&.uniq(&:description)
|
58
|
+
end
|
70
59
|
|
71
|
-
|
60
|
+
def clone
|
61
|
+
clone = super
|
72
62
|
|
73
|
-
|
63
|
+
clone.entries = clone.entries.map(&:clone)
|
64
|
+
clone
|
74
65
|
end
|
75
66
|
|
76
|
-
def
|
77
|
-
|
78
|
-
end
|
67
|
+
def entries=(entries)
|
68
|
+
entries ||= []
|
79
69
|
|
80
|
-
|
81
|
-
|
82
|
-
|
70
|
+
raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array)
|
71
|
+
raise ArgumentError, 'entries contains the wrong object type' unless entries.all?(Entry)
|
72
|
+
|
73
|
+
@entries = entries.map(&:clone)
|
83
74
|
end
|
84
75
|
|
85
76
|
# Deletes the entry group file from the file system.
|
86
|
-
def delete
|
87
|
-
self.class.delete(time: time)
|
77
|
+
def delete!
|
78
|
+
self.class.delete!(time: time)
|
79
|
+
self.entries = []
|
88
80
|
self
|
89
81
|
end
|
90
82
|
|
91
|
-
def
|
92
|
-
|
83
|
+
def time_formatted
|
84
|
+
formatted_time(time: time)
|
93
85
|
end
|
94
86
|
|
95
87
|
def save!
|
88
|
+
delete! and return if entries.empty?
|
89
|
+
|
96
90
|
validate!
|
97
91
|
Services::EntryGroupWriterService.new(entry_group: self).call
|
92
|
+
self
|
98
93
|
end
|
99
94
|
|
100
95
|
def to_h
|
101
|
-
super.tap do |hash|
|
102
|
-
hash[:entries] = hash[:entries].dup
|
103
|
-
hash[:entries].each_with_index do |entry, index|
|
104
|
-
hash[:entries][index] = entry.to_h
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def check_unique(sha_or_editor_command:, description:)
|
110
|
-
raise ArgumentError, 'sha_or_editor_command is nil' if sha_or_editor_command.nil?
|
111
|
-
raise ArgumentError, 'description is nil' if description.nil?
|
112
|
-
raise ArgumentError, 'sha_or_editor_command is the wrong object type' unless sha_or_editor_command.is_a?(String)
|
113
|
-
raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
|
114
|
-
|
115
|
-
if entries.blank?
|
116
|
-
entry_unique_hash = entry_unique_hash_for(uuid_unique: true, description_unique: true)
|
117
|
-
return entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
|
118
|
-
end
|
119
|
-
|
120
|
-
entry_hash = entries.each_with_object({}) do |entry_group_entry, hash|
|
121
|
-
hash[entry_group_entry.uuid] = entry_group_entry.description
|
122
|
-
end
|
123
|
-
|
124
|
-
# It is possible that sha_or_editor_command may have an editor command (e.g. +|a|add). If this
|
125
|
-
# is the case, just treat it as unique because when the entry is added, it will get a unique uuid.
|
126
|
-
uuid_unique = !sha_or_editor_command.match?(Entry::ENTRY_UUID_REGEX) || !entry_hash.key?(sha_or_editor_command)
|
127
|
-
entry_unique_hash = entry_unique_hash_for(
|
128
|
-
uuid: sha_or_editor_command,
|
129
|
-
uuid_unique: uuid_unique,
|
130
|
-
description: description,
|
131
|
-
description_unique: !entry_hash.value?(description)
|
132
|
-
)
|
133
|
-
entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
|
134
|
-
end
|
135
|
-
|
136
|
-
def entry_unique_hash_for(uuid_unique:, description_unique:, uuid: nil, description: nil)
|
137
96
|
{
|
138
|
-
|
139
|
-
|
140
|
-
description: description,
|
141
|
-
description_unique: description_unique,
|
142
|
-
formatted_time: Support::TimeFormatable.formatted_time(time: time)
|
97
|
+
time: time.dup,
|
98
|
+
entries: entries.map(&:to_h)
|
143
99
|
}
|
144
100
|
end
|
145
101
|
|
146
|
-
|
147
|
-
Struct.new(*entry_unique_hash.keys, keyword_init: true) do
|
148
|
-
def unique?
|
149
|
-
uuid_unique? && description_unique?
|
150
|
-
end
|
151
|
-
|
152
|
-
def uuid_unique?
|
153
|
-
uuid_unique
|
154
|
-
end
|
155
|
-
|
156
|
-
def description_unique?
|
157
|
-
description_unique
|
158
|
-
end
|
159
|
-
|
160
|
-
def messages
|
161
|
-
return [] if unique?
|
102
|
+
private
|
162
103
|
|
163
|
-
|
164
|
-
|
165
|
-
messages = []
|
166
|
-
messages << "#uuid is not unique: \"#{uuid} #{short_description}\"" unless uuid_unique?
|
167
|
-
messages << "#description is not unique: \"#{uuid} #{short_description}\""
|
168
|
-
end
|
169
|
-
end.new(**entry_unique_hash)
|
104
|
+
def ensure_local_time(time)
|
105
|
+
time.nil? ? Time.now : time.dup.localtime
|
170
106
|
end
|
171
107
|
end
|
172
108
|
end
|
@@ -6,6 +6,7 @@ require_relative '../support/configuration'
|
|
6
6
|
|
7
7
|
module Dsu
|
8
8
|
module Services
|
9
|
+
# This class loads an entry group file.
|
9
10
|
class ConfigurationLoaderService
|
10
11
|
include Dsu::Support::Configuration
|
11
12
|
|
@@ -29,9 +30,25 @@ module Dsu
|
|
29
30
|
attr_reader :default_options
|
30
31
|
|
31
32
|
def config_options
|
32
|
-
return
|
33
|
+
return default_config unless config_file?
|
33
34
|
|
34
|
-
@config_options ||=
|
35
|
+
@config_options ||= begin
|
36
|
+
loaded_config = YAML.safe_load(ERB.new(File.read(config_file)).result)
|
37
|
+
loaded_config = update_and_write_config_file!(loaded_config) unless loaded_config.keys == default_config.keys
|
38
|
+
loaded_config
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def update_and_write_config_file!(loaded_config)
|
43
|
+
loaded_config = default_config.merge(loaded_config)
|
44
|
+
# TODO: Make this into a configuration writer service.
|
45
|
+
# TODO: Test this
|
46
|
+
File.write(config_file, loaded_config.to_yaml)
|
47
|
+
loaded_config
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_config
|
51
|
+
Support::Configuration::DEFAULT_DSU_OPTIONS
|
35
52
|
end
|
36
53
|
end
|
37
54
|
end
|
@@ -2,14 +2,18 @@
|
|
2
2
|
|
3
3
|
require_relative '../models/entry'
|
4
4
|
require_relative '../support/colorable'
|
5
|
+
require_relative '../support/configurable'
|
5
6
|
require_relative '../support/say'
|
6
7
|
require_relative '../support/time_formatable'
|
7
|
-
require_relative '
|
8
|
+
require_relative '../views/edited_entries/shared/errors'
|
9
|
+
require_relative '../views/shared/messages'
|
10
|
+
require_relative 'stdout_redirector_service'
|
8
11
|
|
9
12
|
module Dsu
|
10
13
|
module Services
|
11
14
|
class EntryGroupEditorService
|
12
15
|
include Support::Colorable
|
16
|
+
include Support::Configurable
|
13
17
|
include Support::Say
|
14
18
|
include Support::TimeFormatable
|
15
19
|
|
@@ -24,7 +28,7 @@ module Dsu
|
|
24
28
|
|
25
29
|
def call
|
26
30
|
edit_view = render_edit_view
|
27
|
-
edit
|
31
|
+
edit edit_view
|
28
32
|
# NOTE: Return the original entry group object as any permanent changes
|
29
33
|
# will have been applied to it.
|
30
34
|
entry_group
|
@@ -38,110 +42,54 @@ module Dsu
|
|
38
42
|
# and edit it. The edits will be used to update the entry group.
|
39
43
|
def render_edit_view
|
40
44
|
say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
|
41
|
-
|
45
|
+
StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
|
42
46
|
end
|
43
47
|
|
44
|
-
# Writes the temporary file contents to disk and opens it in the editor
|
45
|
-
|
48
|
+
# Writes the temporary file contents to disk and opens it in the editor
|
49
|
+
# for editing. It then copies the changes to the entry group and writes
|
50
|
+
# the changes to the entry group file.
|
51
|
+
def edit(edit_view)
|
52
|
+
entry_group_with_edits = Models::EntryGroup.new(time: entry_group.time)
|
53
|
+
|
46
54
|
Services::TempFileWriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
|
47
|
-
|
48
|
-
|
55
|
+
if Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
|
56
|
+
Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |editor_line|
|
57
|
+
next unless process_description?(editor_line)
|
58
|
+
|
59
|
+
entry_group_with_edits.entries << Models::Entry.new(description: editor_line)
|
60
|
+
end
|
61
|
+
|
62
|
+
process_entry_group!(entry_group_with_edits)
|
63
|
+
else
|
64
|
+
say "Failed to open temporary file in editor '#{configuration[:editor]}'; " \
|
49
65
|
"the system error returned was: '#{$CHILD_STATUS}'.", ERROR
|
50
66
|
say 'Either set the EDITOR environment variable ' \
|
51
67
|
'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
|
52
|
-
say 'Run `$ dsu help config` for more information
|
53
|
-
|
54
|
-
system('dsu help config')
|
55
|
-
|
56
|
-
return # rubocop:disable Lint/NonLocalExitFromIterator: This is not an iterator.
|
68
|
+
say 'Run `$ dsu help config` for more information:', ERROR
|
57
69
|
end
|
58
|
-
|
59
|
-
update_entry_group!(tmp_file_path: tmp_file_path)
|
60
70
|
end
|
61
71
|
end
|
62
72
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |tmp_file_line|
|
68
|
-
next if comment_or_empty?(tmp_file_line: tmp_file_line)
|
69
|
-
|
70
|
-
entry_info = editor_entry_info_from(tmp_file_line: tmp_file_line)
|
71
|
-
next if entry_info.empty?
|
72
|
-
next if delete_entry_cmd?(sha: entry_info[:sha])
|
73
|
-
next unless add_entry_cmd?(sha: entry_info[:sha]) || sha?(sha: entry_info[:sha])
|
74
|
-
|
75
|
-
entry_info[:sha_or_editor_command] = entry_info[:sha]
|
76
|
-
entry_info[:sha] = nil if add_entry_cmd?(sha: entry_info[:sha])
|
77
|
-
|
78
|
-
entry = Models::Entry.new(uuid: entry_info[:sha], description: entry_info[:description])
|
79
|
-
entry_group.check_unique(sha_or_editor_command: entry_info[:sha_or_editor_command],
|
80
|
-
description: entry_info[:description]).tap do |status|
|
81
|
-
entries << entry and next if status.unique?
|
82
|
-
|
83
|
-
errors << status.messages
|
84
|
-
end
|
73
|
+
def process_entry_group!(entry_group_with_edits)
|
74
|
+
if entry_group_with_edits.entries.empty?
|
75
|
+
entry_group.delete!
|
76
|
+
return
|
85
77
|
end
|
86
78
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
79
|
+
if entry_group_with_edits.invalid?
|
80
|
+
header = 'The following ERRORS were encountered; these changes were not saved:'
|
81
|
+
messages = entry_group_with_edits.errors.full_messages
|
82
|
+
Views::Shared::Messages.new(messages: messages, message_type: :error, options: { header: header }).render
|
91
83
|
end
|
92
84
|
|
93
|
-
#
|
94
|
-
entry_group.entries =
|
95
|
-
entry_group.delete and return unless entry_group.entries?
|
96
|
-
|
85
|
+
# Make sure we're saving only valid, unique entries.
|
86
|
+
entry_group.entries = entry_group_with_edits.valid_unique_entries
|
97
87
|
entry_group.save!
|
98
88
|
end
|
99
89
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
def delete_entry_cmd?(sha:)
|
105
|
-
%w[- d delete].include?(sha)
|
106
|
-
end
|
107
|
-
|
108
|
-
def add_entry_cmd?(sha:)
|
109
|
-
%w[+ a add].include?(sha)
|
110
|
-
end
|
111
|
-
|
112
|
-
def comment_or_empty?(tmp_file_line:)
|
113
|
-
['#', nil].include? tmp_file_line[0]
|
114
|
-
end
|
115
|
-
|
116
|
-
def editor_entry_info_from(tmp_file_line:)
|
117
|
-
match_data = tmp_file_line.match(/(\S+)\s(.+)/)
|
118
|
-
{
|
119
|
-
sha: match_data[1]&.strip,
|
120
|
-
description: match_data[2]&.strip
|
121
|
-
}
|
122
|
-
rescue StandardError
|
123
|
-
{}
|
124
|
-
end
|
125
|
-
|
126
|
-
# TODO: Add this to a module.
|
127
|
-
# https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
|
128
|
-
def capture_stdxxx
|
129
|
-
# The output stream must be an IO-like object. In this case we capture it in
|
130
|
-
# an in-memory IO object so we can return the string value. You can assign any
|
131
|
-
# IO object here.
|
132
|
-
string_io = StringIO.new
|
133
|
-
prev_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
|
134
|
-
prev_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
|
135
|
-
yield
|
136
|
-
string_io.string
|
137
|
-
ensure
|
138
|
-
# Restore the previous value of stderr and stdout (typically equal to STDERR).
|
139
|
-
$stdout = prev_stdout
|
140
|
-
$stderr = prev_stderr
|
141
|
-
end
|
142
|
-
|
143
|
-
def configuration
|
144
|
-
@configuration ||= ConfigurationLoaderService.new.call
|
90
|
+
def process_description?(description)
|
91
|
+
description = Models::Entry.clean_description(description)
|
92
|
+
!(description.blank? || description[0] == '#')
|
145
93
|
end
|
146
94
|
end
|
147
95
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Services
|
5
|
+
# This service captures $stdout, resirects it to a StringIO object,
|
6
|
+
# and returns the string value.
|
7
|
+
# https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
|
8
|
+
module StdoutRedirectorService
|
9
|
+
class << self
|
10
|
+
def call
|
11
|
+
raise ArgumentError, 'no block was provided' unless block_given?
|
12
|
+
|
13
|
+
# The output stream must be an IO-like object. In this case we capture it in
|
14
|
+
# an in-memory IO object so we can return the string value. Any IO object can
|
15
|
+
# be used here.
|
16
|
+
string_io = StringIO.new
|
17
|
+
original_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
|
18
|
+
yield
|
19
|
+
string_io.string
|
20
|
+
ensure
|
21
|
+
# Restore the original $stdout.
|
22
|
+
$stdout = original_stdout
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/dsu/subcommands/list.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../base_cli'
|
4
|
+
require_relative '../support/command_options/dsu_times'
|
5
|
+
require_relative '../support/time_formatable'
|
4
6
|
|
5
7
|
module Dsu
|
6
8
|
module Subcommands
|
7
9
|
class List < Dsu::BaseCLI
|
10
|
+
include Support::CommandOptions::DsuTimes
|
11
|
+
include Support::TimeFormatable
|
12
|
+
|
8
13
|
map %w[d] => :date
|
14
|
+
map %w[dd] => :dates
|
9
15
|
map %w[n] => :today
|
10
16
|
map %w[t] => :tomorrow
|
11
17
|
map %w[y] => :yesterday
|
@@ -17,10 +23,8 @@ module Dsu
|
|
17
23
|
LONG_DESC
|
18
24
|
def today
|
19
25
|
time = Time.now
|
20
|
-
sorted_dsu_times_for(times: [time, time.yesterday])
|
21
|
-
|
22
|
-
puts
|
23
|
-
end
|
26
|
+
times = sorted_dsu_times_for(times: [time, time.yesterday])
|
27
|
+
view_list_for(times: times)
|
24
28
|
end
|
25
29
|
|
26
30
|
desc 'tomorrow, t',
|
@@ -30,10 +34,8 @@ module Dsu
|
|
30
34
|
LONG_DESC
|
31
35
|
def tomorrow
|
32
36
|
time = Time.now
|
33
|
-
sorted_dsu_times_for(times: [time.tomorrow, time])
|
34
|
-
|
35
|
-
puts
|
36
|
-
end
|
37
|
+
times = sorted_dsu_times_for(times: [time.tomorrow, time])
|
38
|
+
view_list_for(times: times)
|
37
39
|
end
|
38
40
|
|
39
41
|
desc 'yesterday, y',
|
@@ -43,10 +45,8 @@ module Dsu
|
|
43
45
|
LONG_DESC
|
44
46
|
def yesterday
|
45
47
|
time = Time.now
|
46
|
-
sorted_dsu_times_for(times: [time.yesterday, time.yesterday.yesterday])
|
47
|
-
|
48
|
-
puts
|
49
|
-
end
|
48
|
+
times = sorted_dsu_times_for(times: [time.yesterday, time.yesterday.yesterday])
|
49
|
+
view_list_for(times: times)
|
50
50
|
end
|
51
51
|
|
52
52
|
desc 'date, d DATE',
|
@@ -57,14 +57,82 @@ module Dsu
|
|
57
57
|
LONG_DESC
|
58
58
|
def date(date)
|
59
59
|
time = Time.parse(date)
|
60
|
-
sorted_dsu_times_for(times: [time, time.yesterday])
|
61
|
-
|
62
|
-
|
60
|
+
times = sorted_dsu_times_for(times: [time, time.yesterday])
|
61
|
+
view_list_for(times: times)
|
62
|
+
rescue ArgumentError => e
|
63
|
+
say "Error: #{e.message}", ERROR
|
64
|
+
exit 1
|
65
|
+
end
|
66
|
+
|
67
|
+
desc 'dates, dd OPTIONS',
|
68
|
+
'Displays the DSU entries for the OPTIONS provided'
|
69
|
+
long_desc <<~LONG_DESC
|
70
|
+
NAME
|
71
|
+
\x5
|
72
|
+
`dsu dates|dd OPTIONS` -- will display the DSU entries for the OPTIONS provided.
|
73
|
+
|
74
|
+
SYNOPSIS
|
75
|
+
\x5
|
76
|
+
`dsu dates|dd OPTIONS`
|
77
|
+
|
78
|
+
OPTIONS:
|
79
|
+
\x5
|
80
|
+
-f|--from DATE|MNEMONIC: ?.
|
81
|
+
|
82
|
+
\x5
|
83
|
+
-t|--to DATE|MNEMONIC: ?.
|
84
|
+
|
85
|
+
\x5
|
86
|
+
#{date_option_description}
|
87
|
+
|
88
|
+
\x5
|
89
|
+
#{mneumonic_option_description}
|
90
|
+
LONG_DESC
|
91
|
+
# -f, --from FROM [DATE|MNEMONIC] (e.g. -f, --from 1/1[/yyy]|n|t|y|today|tomorrow|yesterday)
|
92
|
+
option :from, type: :string, aliases: '-f', banner: 'DATE|MNEMONIC'
|
93
|
+
# -t, --to TO [DATE|MNEMONIC] (e.g. -t, --to 1/1[/yyy]|n|t|y|today|tomorrow|yesterday)
|
94
|
+
option :to, type: :string, aliases: '-t', banner: 'DATE|MNEMONIC'
|
95
|
+
|
96
|
+
# Include dates that have no DSU entries.
|
97
|
+
option :include_all, type: :boolean, aliases: '-a'
|
98
|
+
def dates
|
99
|
+
options = configuration.merge(self.options)
|
100
|
+
times = dsu_times_from!(from_command_option: options[:from], to_command_option: options[:to])
|
101
|
+
# Note special sort here, unlike the other commands where rules for
|
102
|
+
# displaying DSU entries are applied; this is more of a list command.
|
103
|
+
times = times_sort(times: times, entries_display_order: entries_display_order)
|
104
|
+
view_entry_groups(times: times, options: options) do |total_entry_groups|
|
105
|
+
nothing_to_display_banner_for(times) if total_entry_groups.zero?
|
63
106
|
end
|
64
107
|
rescue ArgumentError => e
|
65
108
|
say "Error: #{e.message}", ERROR
|
66
109
|
exit 1
|
67
110
|
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
def nothing_to_display_banner_for(entry_group_times)
|
115
|
+
entry_group_times.sort!
|
116
|
+
time_range = "#{formatted_time(time: entry_group_times.first)} " \
|
117
|
+
"through #{formatted_time(time: entry_group_times.last)}"
|
118
|
+
say "(nothing to display for #{time_range})", INFO
|
119
|
+
end
|
120
|
+
|
121
|
+
# This method will unconditionally display the FIRST and LAST entry groups
|
122
|
+
# associated with the times provided by the <times> argument. All other
|
123
|
+
# entry groups will be conditionally displayed based on the :include_all
|
124
|
+
# value in the <options> argument.
|
125
|
+
def view_list_for(times:)
|
126
|
+
options = configuration.merge(self.options)
|
127
|
+
times_first_and_last = [times.first, times.last]
|
128
|
+
times.each do |time|
|
129
|
+
view_options = options.dup
|
130
|
+
view_options[:include_all] = true if times_first_and_last.include?(time)
|
131
|
+
view_entry_group(time: time, options: view_options) do
|
132
|
+
puts
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
68
136
|
end
|
69
137
|
end
|
70
138
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'time'
|
4
|
+
require_relative 'time_mneumonic'
|
5
|
+
require_relative 'time_mneumonics'
|
6
|
+
|
7
|
+
module Dsu
|
8
|
+
module Support
|
9
|
+
module CommandOptions
|
10
|
+
module DsuTimes
|
11
|
+
include Time
|
12
|
+
include TimeMneumonic
|
13
|
+
include TimeMneumonics
|
14
|
+
|
15
|
+
# Returns an array of Time objects. The first element is the from time. The second element is the to time.
|
16
|
+
# Both arguments are expected to be command options that are time strings, time or relative time mneumonics.
|
17
|
+
def dsu_times_from!(from_command_option:, to_command_option:)
|
18
|
+
times = begin
|
19
|
+
from_time = time_from_mneumonic(command_option: from_command_option) if time_mneumonic?(from_command_option)
|
20
|
+
from_time ||= time_from_date_string(command_option: from_command_option)
|
21
|
+
|
22
|
+
to_time = time_from_mneumonic(command_option: to_command_option) if time_mneumonic?(to_command_option)
|
23
|
+
to_time ||= time_from_date_string(command_option: to_command_option)
|
24
|
+
|
25
|
+
[from_time, to_time].sort
|
26
|
+
end
|
27
|
+
|
28
|
+
(times.min.to_date..times.max.to_date).map(&:to_time)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|