dsu 0.1.0.alpha.3 → 0.1.0.alpha.5

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: abbd29418f4cd3c86a0e826ed9a43d7aba9e11d455c1fcd34024eeb6f1cff0b2
4
- data.tar.gz: e99710491e91d8302582bec2e201244616a126d509e8915c57a5b13e54581418
3
+ metadata.gz: d8563782a0b4d22e374313aa3373b38e62dec995a6f6d996eaa5fa9c58b0d1cb
4
+ data.tar.gz: 65ebebe762dac44300a6d163c03275eaffd66018a83f9836b3f12cb6333569ad
5
5
  SHA512:
6
- metadata.gz: 46bb2a7b817f6dcc413dbf343598dd9c016752eb84657f7b47c0bb19a923618c0e7ef0dc0e59f2a410808537ead6b26f2e647faabe93932c931cf1040be63acb
7
- data.tar.gz: 348649411b52ed829e640e7c1e5a7d00c6df99bbaa80051a9f98176d34a0c078db4de035ba2a8137bb3295a57e5a500f223a3c7f7c7bf2868356bc226933467c
6
+ metadata.gz: a412bf735ce124b6df3bb419a1ed925b6b9aaf6cc7e8fdb13773fd883886119d92c03cb4bc68633d57bcfd293c274db16d5504200790f9be65d5d3d3f3e5e21e
7
+ data.tar.gz: bf7ac4a28753385155d6e0cd7f1694723deaa3ad0d0e41205f63446c78a2a8b0b09d42f4e5557d3c3aef4befa8a6d46e568d308a1d9fb6c9a0f4110d301c025d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ ## [0.1.0.alpha.5] - 2023-05-12
2
+ * Changes
3
+ - `dsu edit SUBCOMMAND` will now allow editing of an entry group for a date that does not yet exist. This will allow you to add entries in the editor using `+|a|add DESCRIPTION`. Be sure to follow the instructions in the editor when editing entry group entries.
4
+ - `dsu edit SUBCOMMAND` will gracefully display an error if the entry sha (Entry#uuid) or entry discription (Entry#description) are not unique. In this case, the entry will not be added to the entry group.
5
+ NOTE: Not all edge cases are being handled currently by `dsu edit SUBCOMMAND`.
6
+ - `dsu add OPTION` will raise an error if the entry discription (Entry#description) are not unique. This will be handled gracefully in a future release.
7
+ ## [0.1.0.alpha.4] - 2023-05-09
8
+ * Changes
9
+ - Gemfile gemspec description changes.
1
10
  ## [0.1.0.alpha.3] - 2023-05-09
2
11
  * Changes
3
12
  - Entry groups are now editable using the `dsu edit SUBCOMMAND` command. See the README.md or `dsu help edit` for more information.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dsu (0.1.0.alpha.3)
4
+ dsu (0.1.0.alpha.5)
5
5
  activesupport (~> 7.0, >= 7.0.4)
6
6
  colorize (~> 0.8.1)
7
7
  deco_lite (~> 1.3)
data/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  **NOTE:** This gem is in development (alpha version). Please see the [WIP Notes](#wip-notes) section for current `dsu` features.
13
13
 
14
- ## Quick Start
14
+ ## Help
15
15
 
16
16
  After installation (`gem install dsu`), the first thing you may want to do is run the `dsu` help:
17
17
  ### Displaying Help
@@ -19,7 +19,7 @@ After installation (`gem install dsu`), the first thing you may want to do is ru
19
19
  ```shell
20
20
  #=>
21
21
  Commands:
22
- dsu add, -a [OPTIONS] DESCRIPTION # Adds a DSU entry having DESCRIPTION to the date associated with the given OPTION
22
+ dsu add, -a [OPTIONS] DESCRIPTION # Adds a DSU entry...
23
23
  dsu config, -c SUBCOMMAND # Manage configuration...
24
24
  dsu edit, -e SUBCOMMAND # Edit DSU entries...
25
25
  dsu help [COMMAND] # Describe available...
@@ -32,32 +32,33 @@ Options:
32
32
 
33
33
  The next thing you may want to do is `add` some DSU activities (entries) for a particular day:
34
34
 
35
- ### Adding DSU Entries
35
+ ## Adding DSU Entries
36
36
  `dsu add [OPTIONS] DESCRIPTION`
37
37
 
38
- Adding DSU entry using this command will _add_ the DSU entry for the given day (or date, `-d`), and also _display_ the given day's (or date's, `-d`) DSU entries, as well as the DSU entries for the previous day relative to the given day or date (`-d`).
38
+ Adding DSU entry using this command will _add_ the DSU entry for the given day (or date, `-d`), and also _display_ the given day's (or date's, `-d`) DSU entries, as well as the DSU entries for the previous day relative to the given day or date (`-d`). *NOTE: You cannot add duplicate entry group entries; that is, the entry DESCRIPTION needs to be unique within an entry group.*
39
39
 
40
- #### Today
41
- If you need to add a DSU entry to the current day (today), you can use the `-t, [--today]` option. Today (`-t`) is the default; therefore, the `-t` flag is optional when adding DSU entries for the current day:
40
+ ### Today
41
+ If you need to add a DSU entry to the current day (today), you can use the `-n`|`--today` option. Today (`-n`) is the default; therefore, the `-n` flag is optional when adding DSU entries for the current day:
42
42
 
43
- `$ dsu add [-t] "Pair with John on ticket IN-12345"`
43
+ `$ dsu add -n|-today "Pair with John on ticket IN-12345"`
44
44
 
45
- #### Yesterday
46
- If for some reason you need to add a DSU entry for the previous day, you can use the `-p, [--previous-day]` option:
45
+ ### Yesterday
46
+ If for some reason you need to add a DSU entry for yesterday, you can use the `-y`| `--yesterday` option:
47
47
 
48
- `$ dsu add -p "Pick up ticket IN-12345"`
48
+ `$ dsu add -y|--yesterday "Pick up ticket IN-12345"`
49
49
 
50
- #### Tomorrow
51
- If you need to add a DSU entry for the previous day, you can use the `-n, [--next-day]` option:
50
+ ### Tomorrow
51
+ If you need to add a DSU entry for tomorrow, you can use the `-t`|`--tomorrow` option:
52
52
 
53
- `$ dsu add -n "Pick up ticket IN-12345"`
53
+ `$ dsu add -t|--tomorrow "Pick up ticket IN-12345"`
54
54
 
55
- #### Miscellaneous Date
56
- If you need to add a DSU entry for a date other than yesterday, today or tomorrow, you can use the `-d, [--date=DATE]` option, where DATE is any date string that can be parsed using `Time.parse`. For example: `require 'time'; Time.parse("2023-01-01")`:
55
+ ### Miscellaneous Date
57
56
 
58
57
  `$ dsu add -d "2022-12-31" "Attend company New Years Coffee Meet & Greet"`
59
58
 
60
- ### Displaying DSU Entries
59
+ See the [Dates](#dates) section for more information on acceptable DATE formats used by `dsu`.
60
+
61
+ ## Displaying DSU Entries
61
62
  You can display DSU entries for a particular day or date (`date`) using any of the following commands. When displaying DSU entries for a particular day or date (`date`), `dsu` will display the given day or date's (`date`) DSU entries, as well as the DSU entries for the _previous_ day, relative to the given day or date. If the date or day you are trying to view falls on a weekend or Monday, `dsu` will display back to, and including the weekend and previous Friday inclusive; this is so that you can share what you did over the weekend (if anything) and the previous Friday at your DSU:
62
63
 
63
64
  - `$ dsu list today|n`
@@ -65,7 +66,7 @@ You can display DSU entries for a particular day or date (`date`) using any of t
65
66
  - `$ dsu list yesterday|y`
66
67
  - `$ dsu list date|d DATE`
67
68
 
68
- #### Examples
69
+ ### Examples
69
70
  The following displays the entries for "Today", where `Time.now == '2023-05-06 08:54:57.6861 -0400'`
70
71
 
71
72
  `$ dsu list today`
@@ -82,7 +83,7 @@ Friday, (Yesterday) 2023-05-05
82
83
 
83
84
  `$ dsu list date "2023-05-06"`
84
85
 
85
- 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.`:
86
+ See the [Dates](#dates) section for more information on acceptable DATE formats used by `dsu`.
86
87
 
87
88
  ```shell
88
89
  #=>
@@ -94,9 +95,9 @@ Friday, (Yesterday) 2023-05-05
94
95
  1. edc25a9a Pick up ticket IN-12345
95
96
  2. f7d3018c Attend new hire meet & greet
96
97
  ```
97
- ### Editing DSU Entries
98
+ ## Editing DSU Entries
98
99
 
99
- You can edit DSU entry groups by date. `dsu` will allow you to edit a DSU entry group using the `dsu edit SUBCOMMAND` date (today|tomorrow|yesterday|date DATE) you specify. `dsu edit` will open your DSU entry group entries in your editor, where you'll be able to perform editing functions against one or all of the entries.
100
+ You can edit DSU entry groups by date. `dsu` will allow you to edit a DSU entry group using the `dsu edit SUBCOMMAND` date (today|tomorrow|yesterday|date DATE) you specify. `dsu edit` will open your DSU entry group entries in your editor, where you'll be able to perform editing functions against one or all of the entries. If no entries exist in the entry group, you can add entries using any of the the *add* (`+|a|add`) editor commands, followed by the entry description. *NOTE: you cannot add duplicate entries; that is, the entry SHA and DESCRIPTION need to be unique within an entry group. Non-unique entries will not be added to the entry group.*
100
101
 
101
102
  Note: See the "[Customizing the `dsu` Configuration File](#customizing-the-dsu-configuration-file)"" section to configure `dsu` to use the editor of your choice.
102
103
 
@@ -105,7 +106,7 @@ Note: See the "[Customizing the `dsu` Configuration File](#customizing-the-dsu-c
105
106
  - `$ dsu edit yesterday|y`
106
107
  - `$ dsu edit date|d DATE`
107
108
 
108
- #### Examples
109
+ ### Examples
109
110
 
110
111
  The following will edit your DSU entry group entries for "Today", where `Time.now == '2023-05-09 12:13:45.8273 -0400'`. Simply follow the directions in the editor file, then save and close your editor to apply the changes:
111
112
 
@@ -133,7 +134,7 @@ The following will edit your DSU entry group entries for "Today", where `Time.no
133
134
  # *** When you are done, save and close your editor ***
134
135
  ```
135
136
 
136
- ##### Edit an Entry
137
+ #### Edit an Entry
137
138
 
138
139
  Simply change the entry descripton text.
139
140
 
@@ -143,7 +144,7 @@ from: 3849f0c0 Interative planning meeting 11:00AM.
143
144
  to: 3849f0c0 Interative planning meeting 12:00AM.
144
145
  ```
145
146
 
146
- ##### Add an Entry
147
+ #### Add an Entry
147
148
 
148
149
  Replace the entry `sha` one of the add commands: `[+|a|add]`.
149
150
 
@@ -152,7 +153,7 @@ For example...
152
153
  + Add me to this entry group.
153
154
  ```
154
155
 
155
- ##### Delete an Entry
156
+ #### Delete an Entry
156
157
 
157
158
  Simply delete the entry or replace the entry `sha` one of the delete commands: `[-|d|delete]`.
158
159
 
@@ -167,7 +168,7 @@ from: 3849f0c0 Interative planning meeting 11:00AM.
167
168
  to: - Interative planning meeting 11:00AM.
168
169
  ```
169
170
 
170
- ##### Reorder Entries
171
+ #### Reorder Entries
171
172
 
172
173
  Simply reorder the entries in the editor.
173
174
 
@@ -183,11 +184,11 @@ from: 3849f0c0 Interative planning meeting 11:00AM.
183
184
  3849f0c0 Interative planning meeting 11:00AM.
184
185
  ```
185
186
 
186
- ### Customizing the `dsu` Configuration File
187
+ ## Customizing the `dsu` Configuration File
187
188
 
188
189
  It is **not** recommended that you create and customize a `dsu` configuration file while this gem is in alpha release. This is because changes to what configuration options are available may take place while in alpha that could break `dsu`. If you *do* want to create and customize the `dsu` configuration file reglardless, you may do the following.
189
190
 
190
- #### Initializing/Customizing the `dsu` Configuration File
191
+ ### Initializing/Customizing the `dsu` Configuration File
191
192
 
192
193
  ```shell
193
194
  # Creates a dsu configuration file in your home folder.
@@ -207,25 +208,26 @@ Where `<whoami>` would be your username (`$ whoami` on nix systems)
207
208
 
208
209
  Once the configuration file is created, you can locate where the `dsu` configuration file is located by running `$ dsu config info` and taking note of the confiruration file path. You may then edit this file using your favorite editor.
209
210
 
210
- ##### Configuration File Options
211
- ###### editor
211
+ #### Configuration File Options
212
+
213
+ ##### editor
212
214
  This is the default editor to use when editing entry groups if the EDITOR environment variable on your system is not set.
213
215
 
214
216
  Default: `nano` (you'll need to change the default editor on Windows systems)
215
217
 
216
- ###### entries_display_order
218
+ ##### entries_display_order
217
219
  Valid values are 'asc' and 'desc', and will sort listed DSU entries in ascending or descending order respectfully, by day.
218
220
 
219
221
  Default: `'desc'`
220
222
 
221
- ###### entries_file_name
223
+ ##### entries_file_name
222
224
  The entries file name format. It is recommended that you do not change this. The file name must include `%Y`, `%m` and `%d` `Time` formatting specifiers to make sure the file name is unique and able to be located by `dsu` functions. For example, the default file name is `%Y-%m-%d.json`; however, something like `%m-%d-%Y.json` or `entry-group-%m-%d-%Y.json` would work as well.
223
225
 
224
226
  ATTENTION: Please keep in mind that if you change this value `dsu` will not recognize entry files using a different format. You would (at this time), have to manually rename any existing entry file names to the new format.
225
227
 
226
228
  Default: `'%Y-%m-%d.json'`
227
229
 
228
- ###### entries_folder
230
+ ##### entries_folder
229
231
  This is the folder where `dsu` stores entry files. You may change this to anything you want. `dsu` will create this folder for you, as long as your system's write permissions allow this.
230
232
 
231
233
  ATTENTION: Please keep in mind that if you change this value `dsu` will not be able to find entry files in any previous folder. You would (at this time), have to manually mode any existing entry files to this new folder.
@@ -234,9 +236,18 @@ Default: `'/Users/<whoami>/dsu/entries'` on nix systems.
234
236
 
235
237
  Where `<whoami>` would be your username (`$ whoami` on nix systems)
236
238
 
239
+ ## Dates
240
+
241
+ These notes apply to anywhere DATE is used...
242
+
243
+ 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 the 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.`
244
+
237
245
  ## WIP Notes
238
246
  This gem is in development (alpha release).
239
247
 
248
+ - Not all edge cases are being handled currently by `dsu edit SUBCOMMAND`.
249
+ - `dsu add OPTION` will raise an error if the entry discription (Entry#description) are not unique. This will be handled gracefully in a future release.
250
+
240
251
  ## Installation
241
252
 
242
253
  $ gem install dsu
@@ -2,12 +2,17 @@
2
2
 
3
3
  require 'deco_lite'
4
4
  require 'securerandom'
5
+ require_relative '../support/descriptable'
5
6
 
6
7
  module Dsu
7
8
  module Models
8
9
  class Entry < DecoLite::Model
10
+ include Support::Descriptable
11
+
12
+ ENTRY_UUID_REGEX = /\A[0-9a-f]{8}\z/i
13
+
9
14
  validates :uuid, presence: true, format: {
10
- with: /\A[0-9a-f]{8}\z/i,
15
+ with: ENTRY_UUID_REGEX,
11
16
  message: 'is the wrong format. ' \
12
17
  '0-9, a-f, and 8 characters were expected.' \
13
18
  }
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'deco_lite'
4
- require_relative '../support/entry_group_loadable'
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'
7
7
  require_relative '../services/entry_group_writer_service'
8
+ require_relative '../support/entry_group_loadable'
9
+ require_relative '../support/time_formatable'
8
10
  require_relative '../validators/entries_validator'
9
11
  require_relative '../validators/time_validator'
12
+ require_relative 'entry'
10
13
 
11
14
  module Dsu
12
15
  module Models
13
16
  class EntryGroup < DecoLite::Model
14
17
  extend Support::EntryGroupLoadable
18
+ include Support::TimeFormatable
15
19
 
16
20
  validates_with Validators::EntriesValidator, fields: [:entries]
17
21
  validates_with Validators::TimeValidator, fields: [:time]
@@ -36,6 +40,16 @@ module Dsu
36
40
  Services::EntryGroupDeleterService.new(time: time, options: options).call
37
41
  end
38
42
 
43
+ def edit(time:, options: {})
44
+ # NOTE: Uncomment this line to prohibit edits on
45
+ # Entry Groups that do not exist (i.e. have no entries).
46
+ # return new(time: time) unless exists?(time: time)
47
+
48
+ load(time: time).tap do |entry_group|
49
+ entry_group.edit(options: options)
50
+ end
51
+ end
52
+
39
53
  def exists?(time:)
40
54
  Dsu::Services::EntryGroupReaderService.entry_group_file_exists?(time: time)
41
55
  end
@@ -53,15 +67,18 @@ module Dsu
53
67
  entry_group_hash = entry_group_hash_for(time: time)
54
68
  hydrate_entry_group_hash(entry_group_hash: entry_group_hash, time: time)
55
69
  end
70
+
71
+ def unique?(entry:)
72
+
73
+ end
56
74
  end
57
75
 
58
76
  def required_fields
59
77
  %i[time entries]
60
78
  end
61
79
 
62
- def save!
63
- validate!
64
- Services::EntryGroupWriterService.new(entry_group: self).call
80
+ def edit(options: {})
81
+ Services::EntryGroupEditorService.new(entry_group: self, options: options).call
65
82
  self
66
83
  end
67
84
 
@@ -71,6 +88,15 @@ module Dsu
71
88
  self
72
89
  end
73
90
 
91
+ def entries?
92
+ entries.any?
93
+ end
94
+
95
+ def save!
96
+ validate!
97
+ Services::EntryGroupWriterService.new(entry_group: self).call
98
+ end
99
+
74
100
  def to_h
75
101
  super.tap do |hash|
76
102
  hash[:entries] = hash[:entries].dup
@@ -80,10 +106,67 @@ module Dsu
80
106
  end
81
107
  end
82
108
 
83
- def to_h_localized
84
- to_h.tap do |hash|
85
- hash[:time] = hash[:time].localtime
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)
86
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
+ {
138
+ uuid: uuid,
139
+ uuid_unique: uuid_unique,
140
+ description: description,
141
+ description_unique: description_unique,
142
+ formatted_time: Support::TimeFormatable.formatted_time(time: time)
143
+ }
144
+ end
145
+
146
+ def entry_unique_struct_from(entry_unique_hash:)
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?
162
+
163
+ short_description = Models::Entry.short_description(string: description)
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)
87
170
  end
88
171
  end
89
172
  end
@@ -9,8 +9,6 @@ module Dsu
9
9
  class ConfigurationLoaderService
10
10
  include Dsu::Support::Configuration
11
11
 
12
- attr_reader :default_options
13
-
14
12
  def initialize(default_options: nil)
15
13
  unless default_options.nil? ||
16
14
  default_options.is_a?(Hash) ||
@@ -18,7 +16,7 @@ module Dsu
18
16
  raise ArgumentError, 'default_options must be a Hash or ActiveSupport::HashWithIndifferentAccess'
19
17
  end
20
18
 
21
- @default_options ||= default_options || {}
19
+ @default_options = default_options || {}
22
20
  @default_options = @default_options.with_indifferent_access if @default_options.is_a?(Hash)
23
21
  end
24
22
 
@@ -28,6 +26,8 @@ module Dsu
28
26
 
29
27
  private
30
28
 
29
+ attr_reader :default_options
30
+
31
31
  def config_options
32
32
  return Support::Configuration::DEFAULT_DSU_OPTIONS unless config_file?
33
33
 
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/entry'
4
+ require_relative '../support/colorable'
5
+ require_relative '../support/say'
6
+ require_relative '../support/time_formatable'
7
+ require_relative 'configuration_loader_service'
8
+
9
+ module Dsu
10
+ module Services
11
+ class EntryGroupEditorService
12
+ include Support::Colorable
13
+ include Support::Say
14
+ include Support::TimeFormatable
15
+
16
+ def initialize(entry_group:, options: {})
17
+ raise ArgumentError, 'entry_group is nil' if entry_group.nil?
18
+ raise ArgumentError, 'entry_group is the wrong object type' unless entry_group.is_a?(Models::EntryGroup)
19
+ raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash) || options.nil?
20
+
21
+ @entry_group = entry_group
22
+ @options = options || {}
23
+ end
24
+
25
+ def call
26
+ edit_view = render_edit_view
27
+ edit!(edit_view: edit_view)
28
+ # NOTE: Return the original entry group object as any permanent changes
29
+ # will have been applied to it.
30
+ entry_group
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :entry_group, :options
36
+
37
+ # Renders the edit view to a string so we can write it to a temporary file
38
+ # and edit it. The edits will be used to update the entry group.
39
+ def render_edit_view
40
+ say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
41
+ capture_stdxxx { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
42
+ end
43
+
44
+ # Writes the temporary file contents to disk and opens it in the editor.
45
+ def edit!(edit_view:)
46
+ Services::TempFileWriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
47
+ unless Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
48
+ say "Failed to open temporary file in editor '#{configuration[:editor]}';" \
49
+ "the system error returned was: '#{$CHILD_STATUS}'.", ERROR
50
+ say 'Either set the EDITOR environment variable ' \
51
+ 'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
52
+ say 'Run `$ dsu help config` for more information.', ERROR
53
+
54
+ system('dsu help config')
55
+
56
+ return # rubocop:disable Lint/NonLocalExitFromIterator: This is not an iterator.
57
+ end
58
+
59
+ update_entry_group!(tmp_file_path: tmp_file_path)
60
+ end
61
+ end
62
+
63
+ # TODO: Clean this up
64
+ def update_entry_group!(tmp_file_path:)
65
+ errors = []
66
+ entry_group.entries = entries = []
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
85
+ end
86
+
87
+ # Display any errors encountered.
88
+ if errors.any?
89
+ say 'Error: one or more entry values were not unique within the entry group entries:', ERROR
90
+ errors.flatten.each { |message| say "Error: #{message}", ERROR }
91
+ end
92
+
93
+ # Save or delete any entries.
94
+ entry_group.entries = entries
95
+ entry_group.delete and return unless entry_group.entries?
96
+
97
+ entry_group.save!
98
+ end
99
+
100
+ def sha?(sha:)
101
+ sha.match?(Models::Entry::ENTRY_UUID_REGEX)
102
+ end
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
145
+ end
146
+ end
147
+ end
148
+ end
@@ -26,6 +26,7 @@ module Dsu
26
26
  entry_group.validate!
27
27
  create_entry_group_path_if!
28
28
  write_entry_group_to_file!
29
+ entry_group
29
30
  rescue ActiveModel::ValidationError
30
31
  puts "Error(s) encountered: #{entry_group.errors.full_messages}"
31
32
  raise
@@ -3,29 +3,29 @@
3
3
  module Dsu
4
4
  module Services
5
5
  class TempFileReaderService
6
- def initialize(temp_file_path:, options: {})
7
- raise ArgumentError, 'temp_file_path is nil' if temp_file_path.nil?
8
- raise ArgumentError, 'temp_file_path is the wrong object type' unless temp_file_path.is_a?(String)
9
- raise ArgumentError, 'temp_file_path is empty' if temp_file_path.empty?
10
- raise ArgumentError, 'temp_file_path does not exist' unless File.exist?(temp_file_path)
6
+ def initialize(tmp_file_path:, options: {})
7
+ raise ArgumentError, 'tmp_file_path is nil' if tmp_file_path.nil?
8
+ raise ArgumentError, 'tmp_file_path is the wrong object type' unless tmp_file_path.is_a?(String)
9
+ raise ArgumentError, 'tmp_file_path is empty' if tmp_file_path.empty?
10
+ raise ArgumentError, 'tmp_file_path does not exist' unless File.exist?(tmp_file_path)
11
11
  raise ArgumentError, 'options is nil' if options.nil?
12
12
  raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
13
13
 
14
- @temp_file_path = temp_file_path
14
+ @tmp_file_path = tmp_file_path
15
15
  @options = options || {}
16
16
  end
17
17
 
18
18
  def call
19
19
  raise ArgumentError, 'no block given' unless block_given?
20
20
 
21
- File.foreach(temp_file_path) do |line|
21
+ File.foreach(tmp_file_path) do |line|
22
22
  yield line.strip
23
23
  end
24
24
  end
25
25
 
26
26
  private
27
27
 
28
- attr_reader :temp_file_path, :options
28
+ attr_reader :tmp_file_path, :options
29
29
  end
30
30
  end
31
31
  end
@@ -5,13 +5,13 @@ require 'tempfile'
5
5
  module Dsu
6
6
  module Services
7
7
  class TempFileWriterService
8
- def initialize(temp_file_content:, options: {})
9
- raise ArgumentError, 'temp_file_content is nil' if temp_file_content.nil?
10
- raise ArgumentError, 'temp_file_content is the wrong object type' unless temp_file_content.is_a?(String)
8
+ def initialize(tmp_file_content:, options: {})
9
+ raise ArgumentError, 'tmp_file_content is nil' if tmp_file_content.nil?
10
+ raise ArgumentError, 'tmp_file_content is the wrong object type' unless tmp_file_content.is_a?(String)
11
11
  raise ArgumentError, 'options is nil' if options.nil?
12
12
  raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
13
13
 
14
- @temp_file_content = temp_file_content
14
+ @tmp_file_content = tmp_file_content
15
15
  @options = options || {}
16
16
  end
17
17
 
@@ -19,7 +19,7 @@ module Dsu
19
19
  raise ArgumentError, 'no block given' unless block_given?
20
20
 
21
21
  Tempfile.new('dsu').tap do |file|
22
- file.write("#{temp_file_content}\n")
22
+ file.write("#{tmp_file_content}\n")
23
23
  file.close
24
24
  yield file.path
25
25
  end.unlink
@@ -27,7 +27,7 @@ module Dsu
27
27
 
28
28
  private
29
29
 
30
- attr_reader :temp_file_content, :options
30
+ attr_reader :tmp_file_content, :options
31
31
  end
32
32
  end
33
33
  end
@@ -1,19 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'English'
4
3
  require_relative '../base_cli'
5
4
  require_relative '../models/entry_group'
6
- require_relative '../services/temp_file_reader_service'
7
- require_relative '../services/temp_file_writer_service'
8
- require_relative '../support/time_formatable'
9
- require_relative '../views/entry_group/edit'
10
5
  require_relative '../views/entry_group/show'
11
6
 
12
7
  module Dsu
13
8
  module Subcommands
14
9
  class Edit < Dsu::BaseCLI
15
- include Support::TimeFormatable
16
-
17
10
  map %w[d] => :date
18
11
  map %w[n] => :today
19
12
  map %w[t] => :tomorrow
@@ -25,7 +18,8 @@ module Dsu
25
18
  Edits the DSU entries for today.
26
19
  LONG_DESC
27
20
  def today
28
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.now)).render
21
+ entry_group = Models::EntryGroup.edit(time: Time.now)
22
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
29
23
  end
30
24
 
31
25
  desc 'tomorrow, t',
@@ -34,7 +28,8 @@ module Dsu
34
28
  Edits the DSU entries for tomorrow.
35
29
  LONG_DESC
36
30
  def tomorrow
37
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.now.tomorrow)).render
31
+ entry_group = Models::EntryGroup.edit(time: Time.now.tomorrow)
32
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
38
33
  end
39
34
 
40
35
  desc 'yesterday, y',
@@ -43,7 +38,8 @@ module Dsu
43
38
  Edits the DSU entries for yesterday.
44
39
  LONG_DESC
45
40
  def yesterday
46
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.now.yesterday)).render
41
+ entry_group = Models::EntryGroup.edit(time: Time.now.yesterday)
42
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
47
43
  end
48
44
 
49
45
  desc 'date, d DATE',
@@ -54,79 +50,12 @@ module Dsu
54
50
  \x5 #{date_option_description}
55
51
  LONG_DESC
56
52
  def date(date)
57
- Views::EntryGroup::Show.new(entry_group: edit_entry_group(time: Time.parse(date))).render
53
+ entry_group = Models::EntryGroup.edit(time: Time.parse(date))
54
+ Views::EntryGroup::Show.new(entry_group: entry_group).render
58
55
  rescue ArgumentError => e
59
56
  say "Error: #{e.message}", ERROR
60
57
  exit 1
61
58
  end
62
-
63
- private
64
-
65
- def edit_entry_group(time:)
66
- formatted_time = formatted_time(time: time)
67
- unless Models::EntryGroup.exists?(time: time)
68
- say "No DSU entries exist for #{formatted_time}"
69
- exit 1
70
- end
71
-
72
- say "Editing DSU entries for #{formatted_time}..."
73
- entry_group = Models::EntryGroup.load(time: time)
74
-
75
- # This renders the view to a string...
76
- output = capture_stdxxx do
77
- Views::EntryGroup::Edit.new(entry_group: entry_group).render
78
- end
79
- # ...which is then written to a temp file.
80
- Services::TempFileWriterService.new(temp_file_content: output).call do |temp_file_path|
81
- unless system("${EDITOR:-#{configuration[:editor]}} #{temp_file_path}")
82
- say "Failed to open temporary file in editor '#{configuration[:editor]}';" \
83
- "the system error returned was: '#{$CHILD_STATUS}'.", ERROR
84
- say 'Either set the EDITOR environment variable ' \
85
- 'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
86
- say 'Run `$ dsu help config` for more information.', ERROR
87
- system('dsu help config')
88
- exit 1
89
- end
90
- entries = []
91
- Services::TempFileReaderService.new(temp_file_path: temp_file_path).call do |temp_file_line|
92
- # Skip comments and blank lines.
93
- next if ['#', nil].include? temp_file_line[0]
94
-
95
- match_data = temp_file_line.match(/(\S+)\s(.+)/)
96
- # TODO: Error handling if match_data is nil.
97
- entry_sha = match_data[1]
98
- entry_description = match_data[2]
99
-
100
- next if %w[- d delete].include?(entry_sha) # delete the entry
101
-
102
- entry_sha = nil if %w[+ a add].include?(entry_sha) # add the new entry
103
- entries << Models::Entry.new(uuid: entry_sha, description: entry_description)
104
- end
105
-
106
- entry_group.entries = entries
107
-
108
- return entry_group.delete if entries.empty?
109
-
110
- entry_group.save!
111
- end
112
- entry_group
113
- end
114
-
115
- # https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
116
- def capture_stdxxx
117
- # The output stream must be an IO-like object. In this case we capture it in
118
- # an in-memory IO object so we can return the string value. You can assign any
119
- # IO object here.
120
- string_io = StringIO.new
121
- prev_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
122
- prev_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
123
- yield
124
- string_io.string
125
- ensure
126
- # Restore the previous value of stderr and stdout (typically equal to STDERR).
127
- $stdout = prev_stdout
128
- $stderr = prev_stderr
129
- end
130
59
  end
131
60
  end
132
61
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Support
5
+ module Descriptable
6
+ class << self
7
+ def included(mod)
8
+ mod.extend(ClassMethods)
9
+ end
10
+ end
11
+
12
+ def short_description
13
+ return '' if description.blank?
14
+
15
+ self.class.short_description(string: description)
16
+ end
17
+
18
+ module ClassMethods
19
+ def short_description(string:, count: 25, elipsis: '...')
20
+ return elipsis unless string.is_a?(String)
21
+
22
+ elipsis_length = elipsis.length
23
+ count = elipsis_length if count.nil? || count < elipsis_length
24
+
25
+ return string if string.length <= count
26
+
27
+ tokens = string.split
28
+ string = ''
29
+
30
+ return "#{tokens.first[0..(count - elipsis_length)]}#{elipsis}" if tokens.count == 1
31
+
32
+ tokens.each do |token|
33
+ break if string.length + token.length + elipsis_length > count
34
+
35
+ string = "#{string} #{token}"
36
+ end
37
+
38
+ "#{string.strip}#{elipsis}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -24,7 +24,8 @@ module Dsu
24
24
  end
25
25
 
26
26
  validate_entry_types field, entries, record
27
- validate_unique_entry_uuids field, entries, record
27
+ validate_unique_entry_attr :uuid, field, entries, record
28
+ validate_unique_entry_attr :description, field, entries, record
28
29
  end
29
30
  end
30
31
 
@@ -42,17 +43,17 @@ module Dsu
42
43
  end
43
44
  end
44
45
 
45
- def validate_unique_entry_uuids(field, entries, record)
46
+ def validate_unique_entry_attr(attr, field, entries, record)
46
47
  return unless entries.is_a? Array
47
48
 
48
49
  entry_objects = entries.select { |entry| entry.is_a?(Dsu::Models::Entry) }
49
50
 
50
- uuids = entry_objects.map(&:uuid)
51
- return if uuids.uniq.length == uuids.length
51
+ attrs = entry_objects.map(&attr)
52
+ return if attrs.uniq.length == attrs.length
52
53
 
53
- non_unique_uuids = uuids.select { |element| uuids.count(element) > 1 }.uniq
54
- if non_unique_uuids.any?
55
- record.errors.add(field, "contains duplicate UUIDs: #{non_unique_uuids.join(', ')}.",
54
+ non_unique_attrs = attrs.select { |attr| attrs.count(attr) > 1 }.uniq # rubocop:disable Lint/ShadowingOuterLocalVariable
55
+ if non_unique_attrs.any?
56
+ record.errors.add(field, "contains duplicate ##{attr}s: #{non_unique_attrs.join(', ')}.",
56
57
  type: Dsu::Support::FieldErrors::FIELD_DUPLICATE_ERROR)
57
58
  end
58
59
  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 = '0.1.0.alpha.3'
4
+ VERSION = '0.1.0.alpha.5'
5
5
  end
@@ -42,13 +42,15 @@ module Dsu
42
42
 
43
43
  def render_entry_group!
44
44
  say "# Editing DSU Entries for #{formatted_time(time: entry_group.time)}"
45
- say('(no entries available for this day)') and return if entry_group.entries.empty?
46
-
45
+ # TODO: Display entry group entries from the previous DSU date so they can be
46
+ # easily copied over; or, add them to the current entry group entries below as
47
+ # a "# [+|a|add] <entry group from previous DSU entry description>" (e.g. commented
48
+ # out) by default?
47
49
  say ''
48
50
  say '# [SHA/COMMAND] [DESCRIPTION]'
49
51
 
50
52
  entry_group.entries.each do |entry|
51
- say "#{entry.uuid} #{entry.description}"
53
+ say "#{entry.uuid} #{entry.description.strip}"
52
54
  end
53
55
 
54
56
  say ''
@@ -26,13 +26,7 @@ module Dsu
26
26
  end
27
27
 
28
28
  def call
29
- # Just in case the entry group is invalid, we'll
30
- # validate it before displaying it.
31
- entry_group.validate!
32
29
  render_entry_group!
33
- rescue ActiveModel::ValidationError
34
- puts "Error(s) encountered: #{entry_group.errors.full_messages}"
35
- raise
36
30
  end
37
31
  alias render call
38
32
 
@@ -47,7 +41,9 @@ module Dsu
47
41
  entry_group.entries.each_with_index do |entry, index|
48
42
  prefix = "#{format('%03s', index + 1)}. #{entry.uuid}"
49
43
  description = colorize_string(string: entry.description, mode: :bold)
50
- say "#{prefix} #{description}"
44
+ entry_info = "#{prefix} #{description}"
45
+ entry_info = "#{entry_info} (validation failed)" unless entry.valid?
46
+ say entry_info
51
47
  end
52
48
  end
53
49
  end
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: 0.1.0.alpha.3
4
+ version: 0.1.0.alpha.5
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-09 00:00:00.000000000 Z
11
+ date: 2023-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -112,12 +112,18 @@ dependencies:
112
112
  - - "~>"
113
113
  - !ruby/object:Gem::Version
114
114
  version: '1.0'
115
- description: |2
116
- dsu is little gem that helps manage your Agile DSU (Daily Stand Up) participation. How? by providing a simple command line interface (CLI) which allows you to create, read, update, and delete (CRUD) noteworthy activities that you performed during your day. During your DSU, you can then easily recall and share these these activities with your team. Activities are grouped by day and can be viewed in simple text format from the command line. When viewing a particular day, dsu will automatically display the previous day's activities as well. This is useful for remembering what you did yesterday, so you can share your "Today" and "Yesterday" activities with your team during your DSU.
117
-
118
- NOTE: This gem is currently in development (alpha release) and does not provide the ability to UPDATE or DELETE activities. These features will be added in future releases.
119
-
120
- IN ADDITION TO THIS: dsu's current behavior when viewing a particular day is to display the previous day's activities. This behavior is not necessarily ideal when sharing activities for a DSU that occurs on a Monday. This is because Monday's DSU typically includes sharing what you did on last FRIDAY (not necessarily "Yesterday"), as well as what you plan on doing "Today". This behavior will be changed in a future release as well, to display the previous Friday's activities (as well as Saturday and Sunday) if "Today" happens to be Monday.
115
+ description: " dsu is little gem that helps manage your Agile DSU (Daily Stand
116
+ Up) participation. How? by providing a simple command line interface (CLI) which
117
+ allows you to create, read, update, and delete (CRUD) noteworthy activities that
118
+ you performed during your day. During your DSU, you can then easily recall and share
119
+ these these activities with your team. Activities are grouped by day and can be
120
+ viewed in simple text format from the command line. When displaying DSU entries
121
+ for a particular day or date (date), dsu will display the given day or date's (date)
122
+ DSU entries, as well as the DSU entries for the previous day, relative to the given
123
+ day or date. If the date or day you are trying to view falls on a weekend or Monday,
124
+ dsu will display back to, and including the weekend and previous Friday inclusive;
125
+ this is so that you can share what you did over the weekend (if anything) and the
126
+ previous Friday at your DSU.\n"
121
127
  email:
122
128
  - public.gma@gmail.com
123
129
  executables:
@@ -147,6 +153,7 @@ files:
147
153
  - lib/dsu/models/entry_group.rb
148
154
  - lib/dsu/services/configuration_loader_service.rb
149
155
  - lib/dsu/services/entry_group_deleter_service.rb
156
+ - lib/dsu/services/entry_group_editor_service.rb
150
157
  - lib/dsu/services/entry_group_hydrator_service.rb
151
158
  - lib/dsu/services/entry_group_reader_service.rb
152
159
  - lib/dsu/services/entry_group_writer_service.rb
@@ -162,6 +169,7 @@ files:
162
169
  - lib/dsu/support/commander/command_help.rb
163
170
  - lib/dsu/support/commander/subcommand.rb
164
171
  - lib/dsu/support/configuration.rb
172
+ - lib/dsu/support/descriptable.rb
165
173
  - lib/dsu/support/entry_group_fileable.rb
166
174
  - lib/dsu/support/entry_group_loadable.rb
167
175
  - lib/dsu/support/entry_group_viewable.rb