dsu 1.0.0 → 1.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa1b6f69c4de21d556a38cd96f29ba9ad8b7b4b2a9f5d34cda7fccb61f15d63f
4
- data.tar.gz: 7376022ead177228164448d6fa4b8ee554a35611d45450955c586ed1bf84462a
3
+ metadata.gz: a0558e3688ef13d3969d4f42575742be25b847b18e822c7042ed2de38ff6450f
4
+ data.tar.gz: f72bd36cb559f2f99ba51687ffa2d13cb56e94dfe64a80f2634d8ad727fb3531
5
5
  SHA512:
6
- metadata.gz: 7128d03202e5f9536e6fda51c2dab009dd0ed32f56f00e17b6d1c96cb5420daa8dd0ab71c69d25f811ee41acf14cda05d133211f3b9480b70907736201d7162b
7
- data.tar.gz: bc276d357add40377ec88b79b8379fad502cea936440c31a81e7e0ed18c1ad766244f0a09b9eb33a1536a0d60d2c7140b429d5643aa6a3d51b2f362cdec60673
6
+ metadata.gz: bd93da3534ed0d6d11522a4a7e5b9adb9b4ec56eabc0633f5f314ccdaa900f98bb7f451bc86b083bd58b2aafe920ac214ce3b1bd4fee940713af39edcc09d9bd
7
+ data.tar.gz: fe1c030e83a19732cabe66389f9849125e5c72e4e5f23a30de2d2cda3b0a01ed26d9fc63077b67c7e10e7b3f54a9c9ce4d4bf893c7ed432c7b0e1b6f1d62e469
data/CHANGELOG.md CHANGED
@@ -1,3 +1,8 @@
1
+ ## [1.1.0.alpha.1] - 2023-05-23
2
+ * Changes
3
+ - Added new configuration option `carry_over_entries_to_today` (`true|false`, default: `false`); if true, when editing DSU entries **for the first time** on any given day (e.g. `dsu edit today`), DSU entries from the previous day will be copied to the editing session. If there are no DSU entries from the previous day, `dsu` will search backwards up to 7 days to find a DSU date that has entries to copy. If after searching back 7 days, no DSU entries are found, the editor session will simply start with no previous DSU entries.
4
+ - Added new configuration option `include_all` (`true|false`, default: `false`); if true, when using dsu commands that list date ranges (e.g. `dsu list dates`), the displayed list will include dates that have no dsu entries. If false, the displayed list will only include dates that have dsu entries. For all other `dsu list` commands, if true, this option will behave in the aforementioned manner. If false, the displayed list will unconditionally display the first and last dates regardless of whether or not the DSU date has entries or not; all other dates will not be displayed if the DSU date has no entries.
5
+ - Changed the look of the editor template when editing entry group entries.
1
6
  ## [1.0.0] - 2023-05-18
2
7
  * First official release.
3
8
  * NOTE: If you have been using the alpha version of `dsu`, you will need to delete the `entries` folder (e.g. `/Users/<whoami>/dsu/entries` on a nix os) as the old entries .json files are incompatible with this official release.
data/Gemfile.lock CHANGED
@@ -1,10 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dsu (1.0.0)
4
+ dsu (1.1.0.alpha.1)
5
+ activemodel (~> 7.0, >= 7.0.4.3)
5
6
  activesupport (~> 7.0, >= 7.0.4)
6
7
  colorize (~> 0.8.1)
7
- deco_lite (~> 1.3)
8
8
  os (~> 1.1, >= 1.1.4)
9
9
  thor (~> 1.2, >= 1.2.1)
10
10
  thor_nested_subcommand (~> 1.0)
@@ -24,11 +24,6 @@ GEM
24
24
  coderay (1.1.3)
25
25
  colorize (0.8.1)
26
26
  concurrent-ruby (1.2.2)
27
- deco_lite (1.5.3)
28
- activemodel (~> 7.0, >= 7.0.3.1)
29
- activesupport (~> 7.0, >= 7.0.3.1)
30
- immutable_struct_ex (~> 0.2.0)
31
- mad_flatter (~> 2.0)
32
27
  diff-lcs (1.5.0)
33
28
  docile (1.4.0)
34
29
  dotenv (2.8.1)
@@ -37,12 +32,8 @@ GEM
37
32
  ffaker (2.21.0)
38
33
  i18n (1.13.0)
39
34
  concurrent-ruby (~> 1.0)
40
- immutable_struct_ex (0.2.3)
41
35
  json (2.6.3)
42
36
  kwalify (0.7.2)
43
- mad_flatter (2.0.0)
44
- activesupport (~> 7.0, >= 7.0.3.1)
45
- immutable_struct_ex (~> 0.2.0)
46
37
  method_source (1.0.0)
47
38
  minitest (5.18.0)
48
39
  os (1.1.4)
@@ -76,7 +67,7 @@ GEM
76
67
  diff-lcs (>= 1.2.0, < 2.0)
77
68
  rspec-support (~> 3.12.0)
78
69
  rspec-support (3.12.0)
79
- rubocop (1.50.2)
70
+ rubocop (1.51.0)
80
71
  json (~> 2.3)
81
72
  parallel (~> 1.10)
82
73
  parser (>= 3.2.0.0)
@@ -90,9 +81,9 @@ GEM
90
81
  parser (>= 3.2.1.0)
91
82
  rubocop-capybara (2.18.0)
92
83
  rubocop (~> 1.41)
93
- rubocop-factory_bot (2.22.0)
84
+ rubocop-factory_bot (2.23.1)
94
85
  rubocop (~> 1.33)
95
- rubocop-performance (1.17.1)
86
+ rubocop-performance (1.18.0)
96
87
  rubocop (>= 1.7.0, < 2.0)
97
88
  rubocop-ast (>= 0.4.0)
98
89
  rubocop-rspec (2.22.0)
@@ -106,7 +97,7 @@ GEM
106
97
  simplecov_json_formatter (~> 0.1)
107
98
  simplecov-html (0.12.3)
108
99
  simplecov_json_formatter (0.1.4)
109
- thor (1.2.1)
100
+ thor (1.2.2)
110
101
  thor_nested_subcommand (1.0.0)
111
102
  tzinfo (2.0.6)
112
103
  concurrent-ruby (~> 1.0)
data/README.md CHANGED
@@ -71,12 +71,11 @@ The following displays the entries for "Today", where `Time.now == '2023-05-06 0
71
71
  ```shell
72
72
  #=>
73
73
  Saturday, (Today) 2023-05-06
74
- 1. 587a2f29 Blocked for locally failing test IN-12345
75
- Hope to pair with John on it
74
+ 1. Blocked for locally failing test IN-12345
76
75
 
77
76
  Friday, (Yesterday) 2023-05-05
78
- 1. edc25a9a Pick up ticket IN-12345
79
- 2. f7d3018c Attend new hire meet & greet
77
+ 1. Pick up ticket IN-12345
78
+ 2. Attend new hire meet & greet
80
79
  ```
81
80
 
82
81
  `$ dsu list date "2023-05-06"`
@@ -86,12 +85,11 @@ See the [Dates](#dates) section for more information on acceptable DATE formats
86
85
  ```shell
87
86
  #=>
88
87
  Saturday, (Today) 2023-05-06
89
- 1. 587a2f29 Blocked for locally failing test IN-12345
90
- Hope to pair with John on it
88
+ 1. Blocked for locally failing test IN-12345
91
89
 
92
90
  Friday, (Yesterday) 2023-05-05
93
- 1. edc25a9a Pick up ticket IN-12345
94
- 2. f7d3018c Attend new hire meet & greet
91
+ 1. Pick up ticket IN-12345
92
+ 2. Attend new hire meet & greet
95
93
  ```
96
94
  ## Editing DSU Entries
97
95
 
@@ -115,6 +113,7 @@ The following will edit your DSU entry group entries for "Today", where `Time.no
115
113
  #=> In your editor, you will see...
116
114
  # Editing DSU Entries for Tuesday, (Today) 2023-05-09 EDT
117
115
  # [ENTRY DESCRIPTION]
116
+
118
117
  Interative planning meeting 11:00AM.
119
118
  Pair with Chad on ticket 31211.
120
119
  Investigate spike ticket 31255.
data/lib/dsu/base_cli.rb CHANGED
@@ -30,9 +30,19 @@ module Dsu
30
30
  end
31
31
 
32
32
  def date_option_description
33
- <<-DATE_OPTION_DESC
34
- Where DATE may be any date string that can be parsed using `Time.parse`. Consequently, you may use also use '/' as date separators, as well as omit thee year if the date you want to display is the current year (e.g. <month>/<day>, or 1/31). For example: `require 'time'; Time.parse('2023-01-02'); Time.parse('1/2') # etc.`
35
- DATE_OPTION_DESC
33
+ <<-OPTION_DESC
34
+ DATE:
35
+ \x5
36
+ This may be any date string that can be parsed using `Time.parse`. Consequently, you may use also use '/' as date separators, as well as omit thee year if the date you want to display is the current year (e.g. <month>/<day>, or 1/31). For example: `require 'time'; Time.parse('01/02/2023'); Time.parse('1/2') # etc.`
37
+ OPTION_DESC
38
+ end
39
+
40
+ def mneumonic_option_description
41
+ <<-OPTION_DESC
42
+ MNEUMONIC:
43
+ \x5
44
+ This may be any of the following: DATE (see DATE)|n|today|t|tomorrow|y|yesterday.
45
+ OPTION_DESC
36
46
  end
37
47
  end
38
48
 
data/lib/dsu/cli.rb CHANGED
@@ -20,7 +20,7 @@ module Dsu
20
20
  long_desc <<-LONG_DESC
21
21
  NAME
22
22
  \x5
23
- `DSU add, -a [OPTIONS] DESCRIPTION` -- will add a DSU entry having DESCRIPTION to the date associated with the given OPTION.
23
+ `dsu add, -a [OPTIONS] DESCRIPTION` -- will add a DSU entry having DESCRIPTION to the date associated with the given OPTION.
24
24
 
25
25
  SYNOPSIS
26
26
  \x5
@@ -54,14 +54,12 @@ module Dsu
54
54
  def add(description)
55
55
  time = if options[:date].present?
56
56
  Time.parse(options[:date])
57
- else
58
- if options[:tomorrow].present?
59
- Time.now.tomorrow
60
- elsif options[:yesterday].present?
61
- Time.now.yesterday
62
- elsif options[:today].present?
63
- Time.now
64
- end
57
+ elsif options[:tomorrow].present?
58
+ Time.now.tomorrow
59
+ elsif options[:yesterday].present?
60
+ Time.now.yesterday
61
+ elsif options[:today].present?
62
+ Time.now
65
63
  end
66
64
  entry = Models::Entry.new(description: description)
67
65
  CommandServices::AddEntryService.new(entry: entry, time: time).call
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/date/calculations'
4
+
5
+ module DateAndTime
6
+ module Calculations
7
+ def not_today?
8
+ !today?
9
+ end
10
+ end
11
+ end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_model'
4
- require 'securerandom'
5
4
  require_relative '../support/descriptable'
6
5
  require_relative '../validators/description_validator'
7
6
 
@@ -51,19 +51,6 @@ module Dsu
51
51
  def exists?(time:)
52
52
  Dsu::Services::EntryGroupReaderService.entry_group_file_exists?(time: time)
53
53
  end
54
-
55
- # Loads the EntryGroup from the file system and returns an
56
- # instantiated EntryGroup object.
57
- def load(time: nil)
58
- load_entry_group_file_for(time: time)
59
- end
60
-
61
- # This function returns a hash whose :time and :entries
62
- # key values are hydrated with instantiated Time and Entry
63
- # objects.
64
- def load_entry_group_for(time:)
65
- load_entry_group_file_for(time: time)
66
- end
67
54
  end
68
55
 
69
56
  def valid_unique_entries
@@ -93,6 +80,10 @@ module Dsu
93
80
  self
94
81
  end
95
82
 
83
+ def time_formatted
84
+ formatted_time(time: time)
85
+ end
86
+
96
87
  def save!
97
88
  delete! and return if entries.empty?
98
89
 
@@ -30,9 +30,25 @@ module Dsu
30
30
  attr_reader :default_options
31
31
 
32
32
  def config_options
33
- return Support::Configuration::DEFAULT_DSU_OPTIONS unless config_file?
33
+ return default_config unless config_file?
34
34
 
35
- @config_options ||= YAML.safe_load(ERB.new(File.read(config_file)).result)
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
36
52
  end
37
53
  end
38
54
  end
@@ -2,17 +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
8
  require_relative '../views/edited_entries/shared/errors'
8
9
  require_relative '../views/shared/messages'
9
- require_relative 'configuration_loader_service'
10
10
  require_relative 'stdout_redirector_service'
11
11
 
12
12
  module Dsu
13
13
  module Services
14
14
  class EntryGroupEditorService
15
15
  include Support::Colorable
16
+ include Support::Configurable
16
17
  include Support::Say
17
18
  include Support::TimeFormatable
18
19
 
@@ -90,10 +91,6 @@ module Dsu
90
91
  description = Models::Entry.clean_description(description)
91
92
  !(description.blank? || description[0] == '#')
92
93
  end
93
-
94
- def configuration
95
- @configuration ||= ConfigurationLoaderService.new.call
96
- end
97
94
  end
98
95
  end
99
96
  end
@@ -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]).each do |t|
21
- view_entry_group(time: t)
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]).each do |t|
34
- view_entry_group(time: t)
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]).each do |t|
47
- view_entry_group(time: t)
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]).each do |t|
61
- view_entry_group(time: t)
62
- puts
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
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Support
5
+ module CommandOptions
6
+ # The purpose of this module is to take a command option that is a string and return a Time object.
7
+ # The command option is expected to be a date in the format of [M]M/[D]D[/YYYY]. MM and DD with
8
+ # leading zeroes is optional (i.e. only M and D are required), YYYY is optionl and will be replaced
9
+ # with the current year if not provided.
10
+ module Time
11
+ DATE_CAPTURE_REGEX = %r{\A(?<month>0?[1-9]|1[0-2])/(?<day>0?[1-9]|1\d|2\d|3[01])(?:/(?<year>\d{4}))?\z}
12
+
13
+ def time_from_date_string!(command_option:)
14
+ raise ArgumentError, 'command_option is nil.' if command_option.nil?
15
+ raise ArgumentError, 'command_option is blank.' if command_option.blank?
16
+
17
+ unless command_option.is_a?(String)
18
+ raise ArgumentError, "command_option is not a String: \"#{command_option}\"."
19
+ end
20
+
21
+ time_parts = time_parts_for(time_string: command_option)
22
+ return unless time_parts?(time_parts: time_parts)
23
+
24
+ valid_time!(time_parts: time_parts)
25
+
26
+ # This will rescue errors resulting from calling Date.strptime with an invalid date string,
27
+ # and return a more meaningful error message.
28
+ rescue DateTime::Error
29
+ raise ArgumentError, "command_option is not a valid date: \"#{command_option}\"."
30
+ end
31
+
32
+ def time_from_date_string(command_option:)
33
+ time_from_date_string!(command_option: command_option)
34
+ rescue ArgumentError
35
+ nil
36
+ end
37
+
38
+ private
39
+
40
+ # This method returns the time parts for the given time string in a hash
41
+ # (i.e. month, day, year) IF the time string matches the DATE_CAPTURE_REGEX
42
+ # regex. Otherwise, it returns an empty hash.
43
+ def time_parts_for(time_string:)
44
+ match_data = DATE_CAPTURE_REGEX.match(time_string)
45
+ return {} if match_data.nil?
46
+
47
+ {
48
+ month: match_data[:month],
49
+ day: match_data[:day],
50
+ year: match_data[:year]
51
+ }
52
+ end
53
+
54
+ # This method returns true if the date passes the DATE_CAPTURE_REGEX regex match
55
+ # in #date_parts_for and returns a non-nil hash. Otherwise, it returns false.
56
+ # A non-nil hash returned from #date_parts_for doesn necessarily mean the date
57
+ # parts will equate to a valid date when parsed, it just means the date string
58
+ # matched the regex. Calling #valid_date! will raise an ArgumentError if the
59
+ # date parts do not equate to a valid date.
60
+ def time_parts?(time_parts:)
61
+ !time_parts.empty?
62
+ end
63
+
64
+ def valid_time!(time_parts:)
65
+ time_string = time_string_for(time_parts: time_parts)
66
+ Date.strptime(time_string, '%Y/%m/%d').to_time
67
+ end
68
+
69
+ def time_string_for(time_parts:)
70
+ # Replace the year with the current year if it is nil.
71
+ time_parts[:year] = ::Time.now.year if time_parts[:year].nil?
72
+ "#{time_parts[:year]}/#{time_parts[:month]}/#{time_parts[:day]}"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'time_mneumonics'
4
+
5
+ module Dsu
6
+ module Support
7
+ module CommandOptions
8
+ # The purpose of this module is to take a command option that is a string and return a Time object.
9
+ # The command option is expected to be a time mneumoic.
10
+ module TimeMneumonic
11
+ include TimeMneumonics
12
+
13
+ def time_from_mneumonic(command_option:, relative_time: nil)
14
+ time_from_mneumonic!(command_option: command_option, relative_time: relative_time)
15
+ rescue ArgumentError
16
+ nil
17
+ end
18
+
19
+ # command_option: is expected to me a time mneumonic. If relative_time is NOT nil, all
20
+ # time mneumonics are relative to relative_time. Otherwise, they are relative to Time.now.
21
+ # relative_time: is a Time object that is required IF command_option is expected to be
22
+ # a relative time mneumonic. Otherwise, it is optional.
23
+ def time_from_mneumonic!(command_option:, relative_time: nil)
24
+ validate_argument!(command_option: command_option, command_option_name: :command_option)
25
+ unless relative_time.nil?
26
+ validate_argument!(command_option: relative_time, command_option_name: :relative_time)
27
+ end
28
+
29
+ # if relative_time_mneumonic?(command_option) && !relative_time.nil?
30
+ # # If command_option is a relative time mneumonic, we need to get the time
31
+ # # relative to relative_time using ::Time.now, and use the command_option
32
+ # # as the relative time.
33
+ # return time_from_mneumonic!(command_option: relative_time, relative_time: command_option)
34
+ # end
35
+
36
+ time_for_mneumonic(mneumonic: command_option, relative_time: relative_time)
37
+ end
38
+
39
+ # This method returns true if mneumonic is a valid mneumonic OR
40
+ # a relative time mneumonic.
41
+ def time_mneumonic?(mneumonic)
42
+ mneumonic?(mneumonic) || relative_time_mneumonic?(mneumonic)
43
+ end
44
+
45
+ private
46
+
47
+ # Returns a Time object from a mneumonic.
48
+ def time_for_mneumonic(mneumonic:, relative_time: nil)
49
+ # If relative_time is a relative time mneumonic, then we need to first
50
+ # convert mneumonic to a Time object first, so that we can calculate
51
+ # `relative_time.to_i.days.from_now(time)` to get the correct Time we
52
+ # need.
53
+ if relative_time_mneumonic?(relative_time)
54
+ time = time_for_mneumonic(mneumonic: mneumonic)
55
+ return relative_time_for(days_from_now: relative_time, time: time)
56
+ end
57
+
58
+ if mneumonic?(mneumonic) && mneumonic?(relative_time)
59
+ time = time_for_mneumonic(mneumonic: mneumonic)
60
+
61
+ # Simply return the time if relative_time is 'today'
62
+ # because 'today' relative to any time will always
63
+ # point to itself.
64
+ return time if today_mneumonic?(relative_time)
65
+
66
+ return time.public_send(relative_time)
67
+ end
68
+
69
+ time = ::Time.now
70
+ if today_mneumonic?(mneumonic)
71
+ time
72
+ elsif tomorrow_mneumonic?(mneumonic)
73
+ time.tomorrow
74
+ elsif yesterday_mneumonic?(mneumonic)
75
+ time.yesterday
76
+ elsif relative_time_mneumonic?(mneumonic)
77
+ relative_time_for(days_from_now: mneumonic, time: time)
78
+ end
79
+ end
80
+
81
+ def relative_time_for(days_from_now:, time:)
82
+ days_from_now.to_i.days.from_now(time)
83
+ end
84
+
85
+ # This method returns true if mneumonic is a valid time mneumonic.
86
+ # This method will return false if mneumonic is an invalid mneumonic
87
+ # OR if mneumonic is a relative time mneumonic.
88
+ def mneumonic?(mneumonic)
89
+ today_mneumonic?(mneumonic) ||
90
+ tomorrow_mneumonic?(mneumonic) ||
91
+ yesterday_mneumonic?(mneumonic)
92
+ end
93
+
94
+ # This method returns true if mneumonic is a valid relative
95
+ # time mneumonic.
96
+ def relative_time_mneumonic?(mneumonic)
97
+ return false unless mneumonic.is_a?(String)
98
+
99
+ mneumonic.match?(RELATIVE_REGEX)
100
+ end
101
+
102
+ def today_mneumonic?(mneumonic)
103
+ TODAY.include?(mneumonic)
104
+ end
105
+
106
+ def tomorrow_mneumonic?(mneumonic)
107
+ TOMORROW.include?(mneumonic)
108
+ end
109
+
110
+ def yesterday_mneumonic?(mneumonic)
111
+ YESERDAY.include?(mneumonic)
112
+ end
113
+
114
+ def validate_argument!(command_option:, command_option_name:)
115
+ raise ArgumentError, "#{command_option_name} cannot be nil." if command_option.nil?
116
+ raise ArgumentError, "#{command_option_name} cannot be blank." if command_option.blank?
117
+ unless command_option.is_a?(String)
118
+ raise ArgumentError, "#{command_option_name} must be a String: \"#{command_option}\""
119
+ end
120
+ unless time_mneumonic?(command_option)
121
+ raise ArgumentError, "#{command_option_name} is an invalid mneumonic: \"#{command_option}\"."
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Support
5
+ module CommandOptions
6
+ module TimeMneumonics
7
+ TODAY = %w[n today].freeze
8
+ TOMORROW = %w[t tomorrow].freeze
9
+ YESERDAY = %w[y yesterday].freeze
10
+
11
+ RELATIVE_REGEX = /[+-]\d+/
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../services/configuration_loader_service'
4
+
5
+ module Dsu
6
+ module Support
7
+ # This module provides a way to configure a class, so that it can
8
+ # be used in a test environment.
9
+ module Configurable
10
+ def configuration
11
+ @configuration ||= Services::ConfigurationLoaderService.new.call
12
+ end
13
+ end
14
+ end
15
+ end
@@ -27,7 +27,19 @@ module Dsu
27
27
  # asc or desc, ascending or descending, respectively.
28
28
  'entries_display_order' => 'desc',
29
29
  'entries_file_name' => '%Y-%m-%d.json',
30
- 'entries_folder' => "#{FolderLocations.root_folder}/dsu/entries"
30
+ 'entries_folder' => "#{FolderLocations.root_folder}/dsu/entries",
31
+ 'carry_over_entries_to_today' => false,
32
+ # If true, when using dsu commands that list date ranges (e.g.
33
+ # `dsu list dates`), the displayed list will include dates that
34
+ # have no dsu entries. If false, the displayed list will only
35
+ # include dates that have dsu entries.
36
+ # For all other `dsu list` commands, if true, this option will
37
+ # behave in the aforementioned manner. If false, the displayed
38
+ # list will unconditionally display the first and last dates
39
+ # regardless of whether or not the DSU date has entries or not;
40
+ # all other dates will not be displayed if the DSU date has no
41
+ # entries.
42
+ 'include_all' => false
31
43
  }.freeze
32
44
  # rubocop:enable Style/StringHashKeys
33
45
 
@@ -1,14 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../services/configuration_loader_service'
4
+ require_relative '../support/configurable'
4
5
 
5
6
  module Dsu
6
7
  module Support
8
+ # TODO: I hate this module; refactor it!!!
9
+ # This module expects the following attributes to be defined: :time, :options
7
10
  module EntryGroupFileable
8
- module_function
11
+ extend Support::Configurable
12
+
13
+ class << self
14
+ def entry_group_file_exists?(time:)
15
+ File.exist?(entry_group_file_path(time: time))
16
+ end
17
+
18
+ def entry_group_file_path(time:)
19
+ File.join(entries_folder, entries_file_name(time: time))
20
+ end
21
+
22
+ def entries_folder
23
+ configuration[:entries_folder]
24
+ end
25
+
26
+ def entries_file_name(time:)
27
+ time.strftime(configuration[:entries_file_name])
28
+ end
29
+
30
+ # def configuration
31
+ # Services::ConfigurationLoaderService.new.call
32
+ # end
33
+ end
9
34
 
10
35
  def entry_group_file_exists?
11
- File.exist?(entry_group_file_path)
36
+ EntryGroupFileable.entry_group_file_exists?(time: time)
12
37
  end
13
38
 
14
39
  def entry_group_path_exists?
@@ -16,15 +41,15 @@ module Dsu
16
41
  end
17
42
 
18
43
  def entry_group_file_path
19
- File.join(entries_folder, entries_file_name)
44
+ EntryGroupFileable.entry_group_file_path(time: time)
20
45
  end
21
46
 
22
47
  def entries_folder
23
- @entries_folder ||= configuration[:entries_folder]
48
+ @entries_folder ||= EntryGroupFileable.entries_folder
24
49
  end
25
50
 
26
51
  def entries_file_name
27
- @entries_file_name ||= time.strftime(configuration[:entries_file_name])
52
+ @entries_file_name ||= EntryGroupFileable.entries_file_name(time: time)
28
53
  end
29
54
 
30
55
  def create_entry_group_path_if!
@@ -34,7 +59,7 @@ module Dsu
34
59
  private
35
60
 
36
61
  def configuration
37
- @configuration ||= options[:configuration] || Services::ConfigurationLoaderService.new.call
62
+ @configuration ||= options[:configuration] || EntryGroupFileable.configuration
38
63
  end
39
64
  end
40
65
  end
@@ -8,12 +8,9 @@ require_relative '../models/entry_group'
8
8
  module Dsu
9
9
  module Support
10
10
  module EntryGroupLoadable
11
- module_function
12
-
13
- # returns a Hash having :time and :entries
14
- # where entries == an Array of Entry Hashes
15
- # representing the JSON Entry objects for :time.
16
- def load_entry_group_file_for(time:)
11
+ # returns an EntryGroup object loaded from
12
+ # the entry group json file.
13
+ def load(time:)
17
14
  entry_group_json = Services::EntryGroupReaderService.new(time: time).call
18
15
  hash = if entry_group_json.present?
19
16
  JSON.parse(entry_group_json, symbolize_names: true).tap do |hash|
@@ -26,6 +23,8 @@ module Dsu
26
23
  Models::EntryGroup.new(**hydrate_entry_group_hash(hash: hash, time: time))
27
24
  end
28
25
 
26
+ module_function
27
+
29
28
  # Accepts an entry group hash and returns a
30
29
  # hydrated entry group hash:
31
30
  #
@@ -3,17 +3,35 @@
3
3
  module Dsu
4
4
  module Support
5
5
  module EntryGroupViewable
6
- module_function
7
-
8
- def view_entry_group(time:)
9
- entry_group = if Models::EntryGroup.exists?(time: time)
10
- entry_group_json = Services::EntryGroupReaderService.new(time: time).call
11
- Services::EntryGroupHydratorService.new(entry_group_json: entry_group_json).call
12
- else
13
- Models::EntryGroup.new(time: time)
6
+ def view_entry_groups(times:, options: {})
7
+ total_viewable_entry_groups = 0
8
+
9
+ times.each do |time|
10
+ view_entry_group(time: time, options: options) do
11
+ total_viewable_entry_groups += 1
12
+ puts
13
+ end
14
14
  end
15
+
16
+ yield total_viewable_entry_groups if block_given?
17
+ end
18
+
19
+ def view_entry_group(time:, options: {})
20
+ return unless show_entry_group?(time: time, options: options)
21
+
22
+ entry_group = Models::EntryGroup.load(time: time)
15
23
  Views::EntryGroup::Show.new(entry_group: entry_group).render
24
+
25
+ yield if block_given?
16
26
  end
27
+
28
+ private
29
+
30
+ def show_entry_group?(time:, options:)
31
+ Models::EntryGroup.exists?(time: time) || options[:include_all]
32
+ end
33
+
34
+ module_function :view_entry_group, :view_entry_groups, :show_entry_group?
17
35
  end
18
36
  end
19
37
  end
@@ -18,13 +18,11 @@ module Dsu
18
18
  end
19
19
  end
20
20
 
21
+ # TODO: Do we have something else we can use here?
21
22
  def times_for(times:)
22
23
  start_date = times.max
23
24
  return times unless start_date.monday? || start_date.on_weekend?
24
25
 
25
- # (0..3).map { |num| start_date - num.days }
26
- # (start_date..-start_date.friday?).map { |time| time }
27
- # (0..3).map { |num| start_date - num.days if start_date.on_weekend? || start_date.monday? }
28
26
  # If the start_date is a weekend or a Monday, then we need to include
29
27
  # start_date along with all the dates up to and including the previous
30
28
  # Monday.
@@ -12,7 +12,7 @@ module Dsu
12
12
  def validate(record)
13
13
  unless record.entries.is_a?(Array)
14
14
  record.errors.add(:entries_entry, 'is the wrong object type. ' \
15
- "\"Array\" was expected, but \"#{record.entries.class}\" was received.")
15
+ "\"Array\" was expected, but \"#{record.entries.class}\" was received.")
16
16
  end
17
17
 
18
18
  validate_entry_types record
@@ -27,7 +27,7 @@ module Dsu
27
27
  next if entry.is_a? Dsu::Models::Entry
28
28
 
29
29
  record.errors.add(:entries_entry, 'entry Array element is the wrong object type. ' \
30
- "\"Entry\" was expected, but \"#{entry.class}\" was received.",
30
+ "\"Entry\" was expected, but \"#{entry.class}\" was received.",
31
31
  type: Support::FieldErrors::FIELD_TYPE_ERROR)
32
32
  end
33
33
  end
@@ -43,7 +43,7 @@ module Dsu
43
43
  non_unique_descriptions = descriptions.select { |description| descriptions.count(description) > 1 }.uniq
44
44
  if non_unique_descriptions.any?
45
45
  record.errors.add(:entries_entry, 'contains a duplicate entry: ' \
46
- "#{format_non_unique_descriptions(non_unique_descriptions)}.",
46
+ "#{format_non_unique_descriptions(non_unique_descriptions)}.",
47
47
  type: Support::FieldErrors::FIELD_DUPLICATE_ERROR)
48
48
  end
49
49
  end
data/lib/dsu/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dsu
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0.alpha.1'
5
5
  end
@@ -1,17 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'time'
4
- require 'active_support/core_ext/numeric/time'
5
3
  require_relative '../../models/entry_group'
6
- require_relative '../../support/time_formatable'
4
+ require_relative '../../support/configurable'
7
5
 
8
6
  module Dsu
9
7
  module Views
10
8
  module EntryGroup
11
9
  class Edit
12
- include Support::Colorable
13
- include Support::Say
14
- include Support::TimeFormatable
10
+ include Support::Configurable
15
11
 
16
12
  def initialize(entry_group:, options: {})
17
13
  raise ArgumentError, 'entry_group is nil' if entry_group.nil?
@@ -31,18 +27,16 @@ module Dsu
31
27
  # Just in case the entry group is invalid, we'll validate it before displaying it.
32
28
  entry_group.validate!
33
29
 
34
- # TODO: Display entry group entries from the previous DSU date so they can be
35
- # easily copied over; or, add them to the current entry group entries below as
36
- # a "# [+|a|add] <entry group from previous DSU entry description>"
37
- # (e.g. commented out) by default?
38
-
39
30
  <<~EDIT_VIEW
40
- # Editing DSU Entries for #{formatted_time(time: entry_group.time)}
41
- # [ENTRY DESCRIPTION]
31
+ #{banner_line}
32
+ # Editing DSU Entries for #{entry_group.time_formatted}
33
+ #{banner_line}
42
34
 
43
- #{entry_group_entry_lines.each(&:strip).join("\n")}
35
+ #{entry_group_view&.chomp}
44
36
 
45
- # INSTRUCTIONS:
37
+ #{banner_line}
38
+ # INSTRUCTIONS
39
+ #{banner_line}
46
40
  # ADD a DSU entry: type an ENTRY DESCRIPTION on a new line.
47
41
  # EDIT a DSU entry: change the existing ENTRY DESCRIPTION.
48
42
  # DELETE a DSU entry: delete the ENTRY DESCRIPTION.
@@ -51,6 +45,7 @@ module Dsu
51
45
  # REORDER a DSU entry: reorder the ENTRY DESCRIPTIONs in order preference.
52
46
  #
53
47
  # *** When you are done, save and close your editor ***
48
+ #{banner_line}
54
49
  EDIT_VIEW
55
50
  end
56
51
 
@@ -58,8 +53,68 @@ module Dsu
58
53
 
59
54
  attr_reader :entry_group, :options
60
55
 
56
+ def time
57
+ @time ||= entry_group.time
58
+ end
59
+
60
+ def banner_line
61
+ '#' * 80
62
+ end
63
+
64
+ def entry_group_view
65
+ return entry_group_entry_lines if entry_group.entries.any?
66
+ return previous_entry_group_entry_lines if carry_over_entries_to_today? && previous_entry_group?
67
+
68
+ <<~EDIT_VIEW
69
+ #{banner_line}
70
+ # ENTER DSU ENTRIES BELOW
71
+ #{banner_line}
72
+
73
+ EDIT_VIEW
74
+ end
75
+
61
76
  def entry_group_entry_lines
62
- entry_group.entries.map(&:description)
77
+ raise 'No entries in entry group' if entry_group.entries.empty?
78
+
79
+ <<~EDIT_VIEW
80
+ #{banner_line}
81
+ # DSU ENTRIES
82
+ #{banner_line}
83
+
84
+ #{entry_group.entries.map(&:description).join("\n").chomp}
85
+ EDIT_VIEW
86
+ end
87
+
88
+ def previous_entry_group_entry_lines
89
+ raise 'carry_over_entries_to_today? is false' unless carry_over_entries_to_today?
90
+ raise 'Entries exist in entry_group' if entry_group.entries.any?
91
+ raise 'No previous entry group exists' unless previous_entry_group?
92
+
93
+ <<~EDIT_VIEW
94
+ #{banner_line}
95
+ # PREVIOUS DSU ENTRIES FROM #{previous_entry_group.time_formatted}
96
+ #{banner_line}
97
+
98
+ #{previous_entry_group.entries.map(&:description).join("\n").chomp}
99
+ EDIT_VIEW
100
+ end
101
+
102
+ def previous_entry_group?
103
+ previous_entry_group.present?
104
+ end
105
+
106
+ def previous_entry_group
107
+ # Go back a max of 7 days to find the previous entry group.
108
+ # TODO: Make this configurable or accept an option?
109
+ @previous_entry_group ||= (1..7).each do |days|
110
+ t = time.days_ago(days)
111
+ return Models::EntryGroup.load(time: t) if Support::EntryGroupFileable.entry_group_file_exists?(time: t)
112
+ end
113
+ nil
114
+ end
115
+
116
+ def carry_over_entries_to_today?
117
+ configuration[:carry_over_entries_to_today]
63
118
  end
64
119
  end
65
120
  end
data/lib/dsu.rb CHANGED
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'time'
4
- require 'active_support/core_ext/object/blank'
5
3
  require 'active_support/core_ext/hash/indifferent_access'
6
4
  require 'active_support/core_ext/numeric/time'
5
+ require 'active_support/core_ext/object/blank'
6
+ require 'pry-byebug' if ENV['DEV_ENV']
7
+ require 'thor'
8
+ require 'time'
7
9
 
8
10
  Dir.glob("#{__dir__}/lib/core/**/*.rb").each do |file|
9
11
  require file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dsu
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gene M. Angelo, Jr.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-05-18 00:00:00.000000000 Z
11
+ date: 2023-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -31,33 +31,39 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: 7.0.4
33
33
  - !ruby/object:Gem::Dependency
34
- name: colorize
34
+ name: activemodel
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 0.8.1
39
+ version: '7.0'
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: 7.0.4.3
40
43
  type: :runtime
41
44
  prerelease: false
42
45
  version_requirements: !ruby/object:Gem::Requirement
43
46
  requirements:
44
47
  - - "~>"
45
48
  - !ruby/object:Gem::Version
46
- version: 0.8.1
49
+ version: '7.0'
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: 7.0.4.3
47
53
  - !ruby/object:Gem::Dependency
48
- name: deco_lite
54
+ name: colorize
49
55
  requirement: !ruby/object:Gem::Requirement
50
56
  requirements:
51
57
  - - "~>"
52
58
  - !ruby/object:Gem::Version
53
- version: '1.3'
59
+ version: 0.8.1
54
60
  type: :runtime
55
61
  prerelease: false
56
62
  version_requirements: !ruby/object:Gem::Requirement
57
63
  requirements:
58
64
  - - "~>"
59
65
  - !ruby/object:Gem::Version
60
- version: '1.3'
66
+ version: 0.8.1
61
67
  - !ruby/object:Gem::Dependency
62
68
  name: os
63
69
  requirement: !ruby/object:Gem::Requirement
@@ -149,6 +155,7 @@ files:
149
155
  - lib/dsu/base_cli.rb
150
156
  - lib/dsu/cli.rb
151
157
  - lib/dsu/command_services/add_entry_service.rb
158
+ - lib/dsu/core/ruby/not_today.rb
152
159
  - lib/dsu/models/entry.rb
153
160
  - lib/dsu/models/entry_group.rb
154
161
  - lib/dsu/services/configuration_loader_service.rb
@@ -166,6 +173,11 @@ files:
166
173
  - lib/dsu/subcommands/list.rb
167
174
  - lib/dsu/support/ask.rb
168
175
  - lib/dsu/support/colorable.rb
176
+ - lib/dsu/support/command_options/dsu_times.rb
177
+ - lib/dsu/support/command_options/time.rb
178
+ - lib/dsu/support/command_options/time_mneumonic.rb
179
+ - lib/dsu/support/command_options/time_mneumonics.rb
180
+ - lib/dsu/support/configurable.rb
169
181
  - lib/dsu/support/configuration.rb
170
182
  - lib/dsu/support/descriptable.rb
171
183
  - lib/dsu/support/entry_group_fileable.rb
@@ -204,9 +216,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
204
216
  version: 3.0.1
205
217
  required_rubygems_version: !ruby/object:Gem::Requirement
206
218
  requirements:
207
- - - ">="
219
+ - - ">"
208
220
  - !ruby/object:Gem::Version
209
- version: '0'
221
+ version: 1.3.1
210
222
  requirements: []
211
223
  rubygems_version: 3.3.22
212
224
  signing_key: