dsu 0.1.0.alpha.5 → 1.1.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|