dsu 2.3.1 → 2.4.0

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: a8d5e213674fcc08f03c21defe8712674ebf58ce5d68910d36a8e149f287f8d3
4
- data.tar.gz: ebba09045768d12985eecba52f432edd9461d2814b2f437518f829e6aa3dc9cd
3
+ metadata.gz: '049f61bd321689f9690d42ba2cb72e0901c63e78a1c6454ee8fe0126e8dc0396'
4
+ data.tar.gz: e1687331fa05a352b29800f86b43de93b780a8efa50d74e1be13ce83fa331b23
5
5
  SHA512:
6
- metadata.gz: b4d11fe31ae4bf5e783f3c3b5c064a052f537f23423fd7cb290a8d6d325820bf202ee33dd0b41ab8928651e3272522974efdae58a4ba474d5116cedd2a3fc4a7
7
- data.tar.gz: 174da97c30f0d3467e0c2e22f7d573de4131507f6a26a6be0591c1051457a0edb2bc81ed10b6d54016d019952cfcd705a3beba3029f8fcc259d66d5839b30c68
6
+ metadata.gz: 2d5547b1ce5e805ec2ea8399ed01b76a7c03180b3ddc14b1ad9225cbacec79dbc963b37b24224fac6397b0a88b51772741e0a2840db70064f37f179b2e1d7aa5
7
+ data.tar.gz: a0146a22e7fe3404ee63de7f26d1ce66d945685ec21e41fd1b20eaed4b5cea037920552a5dc52be26c6394dfdf90ff9382d8702ffcfe3b9de571e9b1c0d59e6d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [2.4.0] 2024-01-01
2
+
3
+ Enhancements
4
+
5
+ - Add `dsu import` command to import DSU entries from a comma-delimited csv file. See `dsu help import` for more information.
6
+ - Update README.md to reflect new `dsu import` command.
7
+
8
+ Changes
9
+
10
+ - Update ruby gems.
11
+
12
+ ## [2.3.2] 2023-12-30
13
+
14
+ Changes
15
+
16
+ - Display a "Nothing to export" message if no entries are found, rather than prompting the user "export 0 entry groups" when using the `dsu export` command.
17
+ - Add specs for Export::AllPresenter and Export::DatesPresenter
18
+
1
19
  ## [2.3.1] 2023-12-25
2
20
 
3
21
  Changes
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dsu (2.3.1)
4
+ dsu (2.4.0)
5
5
  activemodel (>= 7.0.8, < 8.0)
6
6
  activesupport (>= 7.0.8, < 8.0)
7
7
  colorize (>= 0.8.1, < 1.0)
@@ -37,13 +37,38 @@ GEM
37
37
  dotenv (2.8.1)
38
38
  drb (2.2.0)
39
39
  ruby2_keywords
40
- factory_bot (6.4.2)
40
+ dry-configurable (1.1.0)
41
+ dry-core (~> 1.0, < 2)
42
+ zeitwerk (~> 2.6)
43
+ dry-core (1.0.1)
44
+ concurrent-ruby (~> 1.0)
45
+ zeitwerk (~> 2.6)
46
+ dry-inflector (1.0.0)
47
+ dry-initializer (3.1.1)
48
+ dry-logic (1.5.0)
49
+ concurrent-ruby (~> 1.0)
50
+ dry-core (~> 1.0, < 2)
51
+ zeitwerk (~> 2.6)
52
+ dry-schema (1.13.3)
53
+ concurrent-ruby (~> 1.0)
54
+ dry-configurable (~> 1.0, >= 1.0.1)
55
+ dry-core (~> 1.0, < 2)
56
+ dry-initializer (~> 3.0)
57
+ dry-logic (>= 1.4, < 2)
58
+ dry-types (>= 1.7, < 2)
59
+ zeitwerk (~> 2.6)
60
+ dry-types (1.7.1)
61
+ concurrent-ruby (~> 1.0)
62
+ dry-core (~> 1.0)
63
+ dry-inflector (~> 1.0)
64
+ dry-logic (~> 1.4)
65
+ zeitwerk (~> 2.6)
66
+ factory_bot (6.4.5)
41
67
  activesupport (>= 5.0.0)
42
68
  ffaker (2.23.0)
43
69
  i18n (1.14.1)
44
70
  concurrent-ruby (~> 1.0)
45
71
  json (2.7.1)
46
- kwalify (0.7.2)
47
72
  language_server-protocol (3.17.0.3)
48
73
  method_source (1.0.0)
49
74
  minitest (5.20.0)
@@ -62,10 +87,11 @@ GEM
62
87
  racc (1.7.3)
63
88
  rainbow (3.1.1)
64
89
  rake (13.1.0)
65
- reek (6.1.4)
66
- kwalify (~> 0.7.0)
90
+ reek (6.2.0)
91
+ dry-schema (~> 1.13.0)
67
92
  parser (~> 3.2.0)
68
93
  rainbow (>= 2.0, < 4.0)
94
+ rexml (~> 3.1)
69
95
  regexp_parser (2.8.3)
70
96
  rexml (3.2.6)
71
97
  rspec (3.12.0)
@@ -114,10 +140,11 @@ GEM
114
140
  simplecov-html (0.12.3)
115
141
  simplecov_json_formatter (0.1.4)
116
142
  thor (1.3.0)
117
- thor_nested_subcommand (1.0.4)
143
+ thor_nested_subcommand (1.0.5)
118
144
  tzinfo (2.0.6)
119
145
  concurrent-ruby (~> 1.0)
120
146
  unicode-display_width (2.5.0)
147
+ zeitwerk (2.6.12)
121
148
 
122
149
  PLATFORMS
123
150
  x86_64-darwin-19
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # `dsu`- Streamline Your Daily Stand-Up Meeting Participation!
2
2
 
3
3
  [![Ruby](https://github.com/gangelo/dsu/actions/workflows/ruby.yml/badge.svg)](https://github.com/gangelo/dsu/actions/workflows/ruby.yml)
4
- [![GitHub version](http://badge.fury.io/gh/gangelo%2Fdsu.svg?refresh=2)](https://badge.fury.io/gh/gangelo%2Fdsu)
5
- [![Gem Version](https://badge.fury.io/rb/dsu.svg?refresh=2)](https://badge.fury.io/rb/dsu)
4
+ [![GitHub version](http://badge.fury.io/gh/gangelo%2Fdsu.svg?refresh=4)](https://badge.fury.io/gh/gangelo%2Fdsu)
5
+ [![Gem Version](https://badge.fury.io/rb/dsu.svg?refresh=4)](https://badge.fury.io/rb/dsu)
6
6
  [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/dsu/)
7
7
  [![Report Issues](https://img.shields.io/badge/report-issues-red.svg)](https://github.com/gangelo/dsu/issues)
8
8
  [![License](http://img.shields.io/badge/license-MIT-yellowgreen.svg)](#license)
@@ -44,6 +44,7 @@ Commands:
44
44
  dsu edit|e SUBCOMMAND # Edit DSU entries...
45
45
  dsu export|x SUBCOMMAND # Export DSU entries...
46
46
  dsu help [COMMAND] # Describe available...
47
+ dsu import|m SUBCOMMAND # Imports DSU entries...
47
48
  dsu info|i # Displays information...
48
49
  dsu list|l SUBCOMMAND # Displays DSU entries...
49
50
  dsu theme|t SUBCOMMAND # Manage DSU themes...
@@ -413,6 +414,43 @@ The following command, when run on December 25, 2023, at 20:15:46...
413
414
 
414
415
  For more information, see `dsu` help (`$ dsu export` or `dsu help export`) for more information.
415
416
 
417
+ ## Importing DSU Entries
418
+
419
+ `dsu` provides a means to import entry group entry data from a previously exported `csv` file (see [Exporting DSU Entries](#exporting-dsu-entries)).
420
+
421
+ If you want to import a previously expoeted `csv` file, you can import `dsu` entries from a `csv` file by using any of the following commands:
422
+
423
+ - `$ dsu import all`
424
+ - `$ dsu m a` # Equivalent to the above, only using shortcuts
425
+ - `$ dsu import dates OPTIONS`
426
+ - `$ dsu m dd OPTIONS` # Equivalent to the above, only using shortcuts
427
+
428
+ **NOTE:** Each `import` command will prompt you to confirm the import. If confirmed, `dsu` will import the entry group entry data from the `csv` file into `dsu`.
429
+
430
+ ### For example
431
+
432
+ ### Importing all entries from a `csv` file
433
+ You can import _all_ entry group entries from a `csv` file.
434
+
435
+ The following command will import all the `dsu` entries from the given `csv` file, and merge the imported entries with any existing entry group entries you may have:
436
+
437
+ `$ dsu import all -i ~/Downloads/dsu-20231225201546-2023-01-01-thru-2024-01-01.csv`
438
+
439
+ The following command will import all the `dsu` entries from the given `csv` file, and **_overwrite_** all entry groups entries with the same entry group date using the `dsu export all` shortcut command:
440
+
441
+ `$ dsu m a -m false -i ~/Downloads/dsu-20231225201546-2023-01-01-thru-2024-01-01.csv`
442
+
443
+ ### Importing specific entries from a `csv` file
444
+ You can import _specific_ entry group entries from a `csv` file for a date range.
445
+
446
+ The following command will import the `dsu` entries from the given `csv` file for the given date range, and merge the imported entries with any existing entry group entries you may have:
447
+
448
+ `$ dsu import dates --from 1/1/2023 --to 12/31/2023 -i ~/Downloads/dsu-20231225201546-2023-01-01-thru-2024-01-01.csv`
449
+
450
+ The following command will import the `dsu` entries from the given `csv` file for the given date range, and **_overwrite_** all entry groups entries with the same entry group date using the `dsu import dates` shortcut command:
451
+
452
+ `$ dsu m dd -m false -f 1/1/2023 -t 12/31/2023 -i ~/Downloads/dsu-20231225201546-2023-01-01-thru-2024-01-01.csv`
453
+
416
454
  ## Customizing the `dsu` Configuration File
417
455
  To customize the `dsu` configuration file, you may follow the instructions outlined here. It is only recommended that you customize the `dsu` configuration file *only* if you are working with an official release (`n.n.n.n`).
418
456
 
data/lib/dsu/cli.rb CHANGED
@@ -8,6 +8,7 @@ require_relative 'subcommands/config'
8
8
  require_relative 'subcommands/delete'
9
9
  require_relative 'subcommands/edit'
10
10
  require_relative 'subcommands/export'
11
+ require_relative 'subcommands/import'
11
12
  require_relative 'subcommands/list'
12
13
  require_relative 'subcommands/theme'
13
14
 
@@ -21,6 +22,7 @@ module Dsu
21
22
  map I18n.t('commands.edit.key_mappings') => :edit
22
23
  map I18n.t('commands.export.key_mappings') => :export
23
24
  map I18n.t('commands.help.key_mappings') => :help
25
+ map I18n.t('commands.import.key_mappings') => :import
24
26
  map I18n.t('commands.info.key_mappings') => :info
25
27
  map I18n.t('commands.list.key_mappings') => :list
26
28
  map I18n.t('commands.theme.key_mappings') => :theme
@@ -68,6 +70,9 @@ module Dsu
68
70
  desc I18n.t('commands.theme.desc'), I18n.t('commands.theme.usage')
69
71
  subcommand :theme, Subcommands::Theme
70
72
 
73
+ desc I18n.t('commands.import.desc'), I18n.t('commands.import.usage')
74
+ subcommand :import, Subcommands::Import
75
+
71
76
  desc I18n.t('commands.info.desc'), I18n.t('commands.info.usage')
72
77
  def info
73
78
  configuration_version = Models::Configuration::VERSION
@@ -54,6 +54,9 @@ module Dsu
54
54
  description: 'Default theme.'
55
55
  }.merge(DEFAULT_THEME_COLORS).freeze
56
56
 
57
+ MIN_DESCRIPTION_LENGTH = 2
58
+ MAX_DESCRIPTION_LENGTH = 256
59
+
57
60
  # TODO: Validate other attrs.
58
61
  validates_with Validators::DescriptionValidator
59
62
  validates_with Validators::ColorThemeValidator
@@ -14,6 +14,9 @@ module Dsu
14
14
  include Support::Descriptable
15
15
  include Support::Presentable
16
16
 
17
+ MIN_DESCRIPTION_LENGTH = 2
18
+ MAX_DESCRIPTION_LENGTH = 256
19
+
17
20
  validates_with Validators::DescriptionValidator
18
21
 
19
22
  attr_reader :description, :options
@@ -191,13 +191,19 @@ module Dsu
191
191
  end
192
192
 
193
193
  def write(file_data:, file_path:)
194
- delete(file_path: file_path) and return true if file_data[:entries].empty?
194
+ if file_data[:entries].empty?
195
+ superclass.delete(file_path: file_path)
196
+ return true
197
+ end
195
198
 
196
199
  super
197
200
  end
198
201
 
199
202
  def write!(file_data:, file_path:)
200
- delete!(file_path: file_path) and return if file_data[:entries].empty?
203
+ if file_data[:entries].empty?
204
+ superclass.delete!(file_path: file_path)
205
+ return
206
+ end
201
207
 
202
208
  super
203
209
  end
@@ -5,6 +5,7 @@ require_relative '../../services/entry_group/exporter_service'
5
5
  require_relative '../../support/ask'
6
6
  require_relative '../base_presenter_ex'
7
7
  require_relative 'messages'
8
+ require_relative 'nothing_to_export'
8
9
  require_relative 'service_callable'
9
10
 
10
11
  module Dsu
@@ -12,6 +13,7 @@ module Dsu
12
13
  module Export
13
14
  class AllPresenter < BasePresenterEx
14
15
  include Messages
16
+ include NothingToExport
15
17
  include ServiceCallable
16
18
  include Support::Ask
17
19
 
@@ -25,7 +27,7 @@ module Dsu
25
27
  end
26
28
 
27
29
  def display_export_prompt
28
- yes?(prompt_with_options(prompt: export_prompt, options: export_prompt_options))
30
+ yes?(prompt_with_options(prompt: export_prompt, options: export_prompt_options), options: options)
29
31
  end
30
32
 
31
33
  private
@@ -4,6 +4,7 @@ require_relative '../../models/entry_group'
4
4
  require_relative '../../support/ask'
5
5
  require_relative '../base_presenter_ex'
6
6
  require_relative 'messages'
7
+ require_relative 'nothing_to_export'
7
8
  require_relative 'service_callable'
8
9
 
9
10
  module Dsu
@@ -11,6 +12,7 @@ module Dsu
11
12
  module Export
12
13
  class DatesPresenter < BasePresenterEx
13
14
  include Messages
15
+ include NothingToExport
14
16
  include ServiceCallable
15
17
  include Support::Ask
16
18
 
@@ -31,7 +33,7 @@ module Dsu
31
33
  end
32
34
 
33
35
  def display_export_prompt
34
- yes?(prompt_with_options(prompt: export_prompt, options: export_prompt_options))
36
+ yes?(prompt_with_options(prompt: export_prompt, options: export_prompt_options), options: options)
35
37
  end
36
38
 
37
39
  private
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../../models/entry_group'
4
- require_relative '../base_presenter_ex'
5
-
6
3
  module Dsu
7
4
  module Presenters
8
5
  module Export
@@ -11,6 +8,10 @@ module Dsu
11
8
  raise NotImplementedError
12
9
  end
13
10
 
11
+ def display_nothing_to_export_message
12
+ puts apply_theme(I18n.t('subcommands.export.messages.nothing_to_export'), theme_color: color_theme.info)
13
+ end
14
+
14
15
  private
15
16
 
16
17
  def display_cancelled_message
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Presenters
5
+ module Export
6
+ module NothingToExport
7
+ def nothing_to_export?
8
+ entry_groups.empty?
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/entry_group'
4
+ require_relative '../../services/entry_group/importer_service'
5
+ require_relative '../../support/ask'
6
+ require_relative '../base_presenter_ex'
7
+ require_relative 'import_file'
8
+ require_relative 'messages'
9
+ require_relative 'service_callable'
10
+
11
+ module Dsu
12
+ module Presenters
13
+ module Import
14
+ class AllPresenter < BasePresenterEx
15
+ include ImportFile
16
+ include Messages
17
+ include ServiceCallable
18
+ include Support::Ask
19
+
20
+ def initialize(import_file_path:, options: {})
21
+ super(options: options)
22
+
23
+ @import_file_path = import_file_path
24
+ end
25
+
26
+ def render(response:)
27
+ return display_cancelled_message unless response
28
+
29
+ display_import_messages importer_service_call
30
+ end
31
+
32
+ def display_import_prompt
33
+ yes?(prompt_with_options(prompt: import_prompt, options: import_prompt_options), options: options)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :import_file_path, :options
39
+
40
+ def import_entry_groups
41
+ @import_entry_groups ||= CSV.foreach(import_file_path,
42
+ headers: true).with_object({}) do |entry_group_entry, entry_groups_hash|
43
+ next unless entry_group_entry['version'].to_i == Dsu::Migration::VERSION
44
+
45
+ Date.parse(entry_group_entry['entry_group']).to_s.tap do |time|
46
+ entry_groups_hash[time] = [] unless entry_groups_hash.key?(time)
47
+ entry_groups_hash[time] << entry_group_entry['entry_group_entry']
48
+ end
49
+ end
50
+ end
51
+
52
+ def import_prompt
53
+ I18n.t('subcommands.import.prompts.import_all_confirm', count: import_entry_groups.count)
54
+ end
55
+
56
+ def import_prompt_options
57
+ I18n.t('subcommands.import.prompts.options')
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../models/entry_group'
4
+ require_relative '../../services/entry_group/importer_service'
5
+ require_relative '../../support/ask'
6
+ require_relative '../base_presenter_ex'
7
+ require_relative 'import_file'
8
+ require_relative 'messages'
9
+ require_relative 'service_callable'
10
+
11
+ module Dsu
12
+ module Presenters
13
+ module Import
14
+ class DatesPresenter < BasePresenterEx
15
+ include ImportFile
16
+ include Messages
17
+ include ServiceCallable
18
+ include Support::Ask
19
+
20
+ def initialize(from:, to:, import_file_path:, options: {})
21
+ super(options: options)
22
+
23
+ @from = from.beginning_of_day
24
+ @to = to.end_of_day
25
+ @import_file_path = import_file_path
26
+ end
27
+
28
+ def render(response:)
29
+ return display_cancelled_message unless response
30
+
31
+ display_import_messages importer_service_call
32
+ end
33
+
34
+ def display_import_prompt
35
+ yes?(prompt_with_options(prompt: import_prompt, options: import_prompt_options), options: options)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :from, :to, :import_file_path, :options
41
+
42
+ def import_entry_groups
43
+ @import_entry_groups ||= CSV.foreach(import_file_path,
44
+ headers: true).with_object({}) do |entry_group_entry, entry_groups_hash|
45
+ next unless entry_group_entry['version'].to_i == Dsu::Migration::VERSION
46
+
47
+ entry_group_time = middle_of_day_for(entry_group_entry['entry_group'])
48
+ next unless entry_group_time.to_date.between?(from.to_date, to.to_date)
49
+
50
+ entry_group_time.to_date.to_s.tap do |time|
51
+ entry_groups_hash[time] = [] unless entry_groups_hash.key?(time)
52
+ entry_groups_hash[time] << entry_group_entry['entry_group_entry']
53
+ end
54
+ end
55
+ end
56
+
57
+ def import_prompt
58
+ I18n.t('subcommands.import.prompts.import_dates_confirm',
59
+ from: from.to_date, to: to.to_date, count: import_entry_groups.keys.count)
60
+ end
61
+
62
+ def import_prompt_options
63
+ I18n.t('subcommands.import.prompts.options')
64
+ end
65
+
66
+ def middle_of_day_for(date_string)
67
+ Time.parse(date_string).in_time_zone.middle_of_day
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Presenters
5
+ module Import
6
+ module ImportFile
7
+ def import_file_path_exist?
8
+ File.exist? import_file_path
9
+ end
10
+
11
+ def nothing_to_import?
12
+ return true unless import_file_path_exist?
13
+
14
+ import_entry_groups.empty?
15
+ end
16
+
17
+ def import_entry_groups
18
+ # Should return a Hash of entry group entries
19
+ # Example: { '2023-12-32' => ['Entry description 1', 'Entry description 2', ...] }
20
+ raise NotImplementedError
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dsu
4
+ module Presenters
5
+ module Import
6
+ module Messages
7
+ def display_import_prompt
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def display_import_file_not_exist_message
12
+ puts apply_theme(I18n.t('subcommands.import.messages.file_not_exist',
13
+ file_path: import_file_path), theme_color: color_theme.info)
14
+ end
15
+
16
+ def display_nothing_to_import_message
17
+ puts apply_theme(I18n.t('subcommands.import.messages.nothing_to_import'), theme_color: color_theme.info)
18
+ end
19
+
20
+ private
21
+
22
+ def display_cancelled_message
23
+ puts apply_theme(I18n.t('subcommands.import.messages.cancelled'), theme_color: color_theme.info)
24
+ end
25
+
26
+ def display_import_messages(import_results)
27
+ import_results.each_pair do |entry_group_date, errors|
28
+ if errors.empty?
29
+ puts apply_theme(I18n.t('subcommands.import.messages.import_success',
30
+ date: entry_group_date), theme_color: color_theme.success)
31
+ else
32
+ errors.each do |error|
33
+ puts apply_theme(I18n.t('subcommands.import.messages.import_error',
34
+ date: entry_group_date, error: error), theme_color: color_theme.error)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../services/entry_group/importer_service'
4
+
5
+ module Dsu
6
+ module Presenters
7
+ module Import
8
+ module ServiceCallable
9
+ private
10
+
11
+ def importer_service_call
12
+ @importer_service_call ||= begin
13
+ importer_service = Services::EntryGroup::ImporterService.new(import_entry_groups: import_entry_groups,
14
+ options: options)
15
+ importer_service.call
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -11,51 +11,69 @@ module Dsu
11
11
  include Support::Fileable
12
12
 
13
13
  def initialize(entry_groups:, options: {})
14
- raise ArgumentError, 'Argument entry_groups is nil' if entry_groups.nil?
14
+ raise ArgumentError, 'Argument entry_groups is blank' if entry_groups.blank?
15
+ raise ArgumentError, 'Argument entry_groups are not all valid' unless entry_groups.all?(&:valid?)
16
+
17
+ validate_entry_group_entries_present! entry_groups
15
18
 
16
19
  @entry_groups = entry_groups
17
20
  @options = options
18
21
  end
19
22
 
20
23
  def call
21
- CSV.open(export_file_name, 'w') do |csv|
24
+ CSV.open(export_file_path, 'w') do |csv|
22
25
  csv << %i[version entry_group entry_no total_entries entry_group_entry]
23
26
 
24
27
  entry_groups.each do |entry_group|
25
- next unless entry_group.exist?
26
-
27
- entry_group.entries.each_with_index do |entry, index|
28
- csv << [
29
- entry_group.version,
30
- entry_group.time.to_date,
31
- index + 1,
32
- entry_group.entries.count,
33
- entry.description
34
- ]
35
- end
28
+ export_entry_group(entry_group: entry_group, csv: csv)
36
29
  end
37
30
  end
38
31
 
39
- export_file_name
32
+ export_file_path
40
33
  end
41
34
 
42
- def export_file_name
43
- @export_file_name ||= begin
44
- file_name = "dsu-#{export_timestamp}-#{times.min.to_date}-thru-#{times.max.to_date}.csv"
45
- File.join(temp_folder, file_name)
46
- end
35
+ def export_file_path
36
+ @export_file_path ||= File.join(temp_folder, export_file_name)
47
37
  end
48
38
 
49
39
  private
50
40
 
51
41
  attr_reader :entry_groups, :options
52
42
 
43
+ def export_entry_data(entry_group:, entry:, entry_index:)
44
+ [
45
+ entry_group.version,
46
+ entry_group.time.to_date,
47
+ entry_index + 1,
48
+ entry_group.entries.count,
49
+ entry.description
50
+ ]
51
+ end
52
+
53
+ def export_entry_group(entry_group:, csv:)
54
+ entry_group.entries.each_with_index do |entry, index|
55
+ csv << export_entry_data(entry_group: entry_group, entry: entry, entry_index: index)
56
+ end
57
+ end
58
+
59
+ def export_file_name
60
+ "dsu-#{timestamp}-#{times.min.to_date}-thru-#{times.max.to_date}.csv"
61
+ end
62
+
53
63
  def times
54
64
  @times ||= entry_groups.map(&:time)
55
65
  end
56
66
 
57
- def export_timestamp
58
- Time.now.in_time_zone.strftime('%Y%m%d%H%M%S')
67
+ def timestamp
68
+ @timestamp ||= Time.now.in_time_zone.strftime('%Y%m%d%H%M%S')
69
+ end
70
+
71
+ def validate_entry_group_entries_present!(entry_groups)
72
+ entry_groups.each do |entry_group|
73
+ next if entry_group.entries.present?
74
+
75
+ raise ArgumentError, "Argument entry_groups entry group for #{entry_group.time_yyyy_mm_dd} has no entries"
76
+ end
59
77
  end
60
78
  end
61
79
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+ require_relative '../../models/entry_group'
5
+
6
+ module Dsu
7
+ module Services
8
+ module EntryGroup
9
+ # Expects a hash having the following format:
10
+ # {
11
+ # "2023-12-29" => ["Entry 1 description", "Entry 2 description", ...],
12
+ # "2023-12-30" => ["Entry 1 description", ...],
13
+ # "2023-12-31" => ["Entry 1 description", ...]
14
+ # }
15
+ class ImporterService
16
+ include Support::Fileable
17
+
18
+ def initialize(import_entry_groups:, options: {})
19
+ raise ArgumentError, 'Argument import_entry_groups is blank' if import_entry_groups.blank?
20
+
21
+ @import_entry_groups = import_entry_groups
22
+ @options = options
23
+ end
24
+
25
+ def call
26
+ import!
27
+ end
28
+
29
+ private
30
+
31
+ attr_reader :import_entry_groups, :options
32
+
33
+ def import!
34
+ import_entry_groups.each_pair do |entry_group_date, entry_descriptions|
35
+ entry_group_for(entry_group_date).tap do |entry_group|
36
+ entry_descriptions.each do |entry_description|
37
+ add_entry_group_entry_if(entry_group: entry_group, entry_description: entry_description)
38
+ end
39
+
40
+ import_messages[entry_group.time_yyyy_mm_dd] = []
41
+
42
+ unless entry_group.save
43
+ entry_group.errors.full_messages.each do |error|
44
+ import_messages[entry_group.time_yyyy_mm_dd] << error
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ import_messages
51
+ end
52
+
53
+ def entry_group_for(entry_group_date)
54
+ time = Time.parse(entry_group_date).in_time_zone
55
+ if merge?
56
+ Models::EntryGroup.find_or_initialize(time: time)
57
+ else
58
+ Models::EntryGroup.new(time: time, options: options)
59
+ end
60
+ end
61
+
62
+ def add_entry_group_entry_if(entry_group:, entry_description:)
63
+ entry = Models::Entry.new(description: entry_description)
64
+ return entry_group.entries << entry if replace?
65
+ return if entry_group.entries.include?(entry)
66
+
67
+ entry_group.entries << entry
68
+ end
69
+
70
+ def merge?
71
+ options.fetch(:merge, true)
72
+ end
73
+
74
+ def replace?
75
+ !merge?
76
+ end
77
+
78
+ def import_messages
79
+ @import_messages ||= {}
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../presenters/export/all_presenter'
4
+ require_relative '../presenters/export/dates_presenter'
3
5
  require_relative '../support/command_options/dsu_times'
4
6
  require_relative '../support/command_options/time_mnemonic'
5
7
  require_relative '../support/time_formatable'
@@ -39,7 +41,6 @@ module Dsu
39
41
  return
40
42
  end
41
43
 
42
- times = times_sort(times: times, entries_display_order: options[:entries_display_order])
43
44
  Views::Export.new(presenter: dates_presenter_for(from: times.min, to: times.max, options: options)).render
44
45
  rescue ArgumentError => e
45
46
  Views::Shared::Error.new(messages: e.message).render
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../presenters/import/all_presenter'
4
+ require_relative '../presenters/import/dates_presenter'
5
+ require_relative '../support/command_options/dsu_times'
6
+ require_relative '../support/command_options/time_mnemonic'
7
+ require_relative '../support/time_formatable'
8
+ require_relative '../views/import'
9
+ require_relative '../views/shared/error'
10
+ require_relative 'base_subcommand'
11
+
12
+ module Dsu
13
+ module Subcommands
14
+ class Import < BaseSubcommand
15
+ include Support::CommandOptions::TimeMnemonic
16
+ include Support::TimeFormatable
17
+
18
+ # TODO: I18n.
19
+ map %w[a] => :all
20
+ map %w[dd] => :dates
21
+
22
+ desc I18n.t('subcommands.import.all.desc'), I18n.t('subcommands.import.all.usage')
23
+ long_desc I18n.t('subcommands.import.all.long_desc')
24
+ option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
25
+ option :merge, type: :boolean, default: true, aliases: '-m'
26
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
27
+ def all
28
+ Views::Import.new(presenter: all_presenter(import_file_path: options[:import_file],
29
+ options: options)).render
30
+ end
31
+
32
+ desc I18n.t('subcommands.import.dates.desc'), I18n.t('subcommands.import.dates.usage')
33
+ long_desc I18n.t('subcommands.import.dates.long_desc',
34
+ date_option_description: date_option_description,
35
+ mnemonic_option_description: mnemonic_option_description)
36
+ option :from, type: :string, required: true, aliases: '-f', banner: 'DATE|MNEMONIC'
37
+ option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
38
+ option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
39
+ option :merge, type: :boolean, default: true, aliases: '-m'
40
+ option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
41
+ def dates
42
+ options = configuration.to_h.merge(self.options).with_indifferent_access
43
+ times, errors = Support::CommandOptions::DsuTimes.dsu_times_for(from_option: options[:from], to_option: options[:to]) # rubocop:disable Layout/LineLength
44
+ if errors.any?
45
+ Views::Shared::Error.new(messages: errors).render
46
+ return
47
+ end
48
+
49
+ Views::Import.new(presenter: dates_presenter_for(from: times.min,
50
+ to: times.max,
51
+ import_file_path: options[:import_file],
52
+ options: options)).render
53
+ rescue ArgumentError => e
54
+ Views::Shared::Error.new(messages: e.message).render
55
+ end
56
+
57
+ private
58
+
59
+ def all_presenter(import_file_path:, options:)
60
+ Presenters::Import::AllPresenter.new(import_file_path: import_file_path, options: options)
61
+ end
62
+
63
+ def dates_presenter_for(from:, to:, import_file_path:, options:)
64
+ Presenters::Import::DatesPresenter.new(from: from, to: to, import_file_path: import_file_path, options: options)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -13,7 +13,10 @@ module Dsu
13
13
  def yes?(prompt, options: {})
14
14
  auto_prompt = auto_prompt(prompt, options)
15
15
 
16
- return auto_prompt unless auto_prompt.nil?
16
+ unless auto_prompt.nil?
17
+ puts prompt
18
+ return auto_prompt
19
+ end
17
20
 
18
21
  Thor::Base.shell.new.yes?(prompt)
19
22
  end
@@ -21,9 +24,10 @@ module Dsu
21
24
  private
22
25
 
23
26
  def auto_prompt(prompt, options)
27
+ options = options.with_indifferent_access
24
28
  prompt = Utils.strip_escapes(prompt)
25
29
  @auto_prompt ||= begin
26
- value = options.dig('prompts', prompt) || options.dig('prompts', 'any')
30
+ value = options.dig(:prompts, prompt) || options.dig(:prompts, :any)
27
31
  value = (value == 'true' unless value.nil?)
28
32
  value
29
33
  end
@@ -21,7 +21,7 @@ module Dsu
21
21
  errors << I18n.t('errors.to_option_invalid', to_option: to_option) if to_time.nil?
22
22
  return [[], errors] if errors.any?
23
23
 
24
- min_time, max_time = [from_time, to_time].sort
24
+ min_time, max_time = [from_time, to_time].minmax
25
25
  [(min_time.to_date..max_time.to_date).map(&:to_time), []]
26
26
  end
27
27
 
@@ -12,6 +12,7 @@ module Dsu
12
12
  end
13
13
 
14
14
  unless description.is_a?(String)
15
+ # TODO: I18n.
15
16
  record.errors.add(:description, 'is the wrong object type. ' \
16
17
  "\"String\" was expected, but \"#{description.class}\" was received.")
17
18
  return
@@ -25,16 +26,26 @@ module Dsu
25
26
  def validate_description(record)
26
27
  description = record.description
27
28
 
28
- return if description.length.between?(2, 256)
29
+ return if description.length.between?(min_description_length(record), max_description_length(record))
29
30
 
30
- if description.length < 2
31
+ if description.length < min_description_length(record)
31
32
  # TODO: I18n.
32
- record.errors.add(:description, "is too short: \"#{record.short_description}\" (minimum is 2 characters).")
33
- elsif description.length > 256
33
+ record.errors.add(:description, "is too short: \"#{record.short_description}\" " \
34
+ "(minimum is #{min_description_length(record)} characters).")
35
+ elsif description.length > max_description_length(record)
34
36
  # TODO: I18n.
35
- record.errors.add(:description, "is too long: \"#{record.short_description}\" (maximum is 256 characters).")
37
+ record.errors.add(:description, "is too long: \"#{record.short_description}\" " \
38
+ "(maximum is #{max_description_length(record)} characters).")
36
39
  end
37
40
  end
41
+
42
+ def min_description_length(record)
43
+ record.class::MIN_DESCRIPTION_LENGTH
44
+ end
45
+
46
+ def max_description_length(record)
47
+ record.class::MAX_DESCRIPTION_LENGTH
48
+ end
38
49
  end
39
50
  end
40
51
  end
@@ -6,6 +6,7 @@ require_relative '../support/field_errors'
6
6
  # https://guides.rubyonrails.org/active_record_validations.html#validates-with
7
7
  module Dsu
8
8
  module Validators
9
+ # TODO: I18n.
9
10
  class EntriesValidator < ActiveModel::Validator
10
11
  include Support::FieldErrors
11
12
 
data/lib/dsu/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Dsu
4
4
  VERSION_REGEX = /\A\d+\.\d+\.\d+(\.(alpha|rc)\.\d+)?\z/
5
- VERSION = '2.3.1'
5
+ VERSION = '2.4.0'
6
6
  end
@@ -14,6 +14,8 @@ module Dsu
14
14
  end
15
15
 
16
16
  def render
17
+ return presenter.display_nothing_to_export_message if presenter.nothing_to_export?
18
+
17
19
  response = presenter.display_export_prompt
18
20
  presenter.render response: response
19
21
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/color_theme'
4
+ require_relative '../models/configuration'
5
+ require_relative '../support/color_themable'
6
+
7
+ module Dsu
8
+ module Views
9
+ class Import
10
+ include Support::ColorThemable
11
+
12
+ def initialize(presenter:)
13
+ @presenter = presenter
14
+ end
15
+
16
+ def render
17
+ return presenter.display_import_file_not_exist_message unless presenter.import_file_path_exist?
18
+ return presenter.display_nothing_to_import_message if presenter.nothing_to_import?
19
+
20
+ response = presenter.display_import_prompt
21
+ presenter.render response: response
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :presenter
27
+ end
28
+ end
29
+ end
@@ -69,6 +69,10 @@ en:
69
69
  key_mappings: x
70
70
  desc: export|x SUBCOMMAND
71
71
  usage: Export DSU entries for the given SUBCOMMAND
72
+ import:
73
+ key_mappings: m
74
+ desc: import|m SUBCOMMAND
75
+ usage: Imports DSU entries for the given SUBCOMMAND
72
76
  info:
73
77
  key_mappings: i
74
78
  desc: info|i
@@ -280,6 +280,7 @@ en:
280
280
  messages:
281
281
  exported: Export successful.
282
282
  exported_to: Entry groups exported to %{file_path}.
283
+ nothing_to_export: No entry groups to export.
283
284
  cancelled: Cancelled.
284
285
  prompts:
285
286
  export_all_confirm: Export all the entries (%{count} entry groups)?
@@ -287,6 +288,98 @@ en:
287
288
  options:
288
289
  - y
289
290
  - N
291
+ import:
292
+ dates:
293
+ desc: dates|dd OPTIONS
294
+ usage: Imports the DSU entries given the OPTIONS provided
295
+ long_desc: |
296
+ Imports the DSU entries for the given OPTIONS provided.
297
+
298
+ $ dsu import dates OPTIONS
299
+
300
+ $ dsu m dd OPTIONS
301
+
302
+ OPTIONS:
303
+
304
+ -i|--import-file IMPORT_CVS_FILE: The IMPORT_CVS_FILE file to import. IMPORT_CVS_FILE should be a fully qualified path to a file that was previously created as a result of running `dsu export`. see `dsu help export`.
305
+
306
+ -m|--merge true|false (default: true): If true, imported entries will be added to the entry group if the entry group already exists. If false, the imported entries will replace all existing entries for the entry group if the entry group already exists. If the entry group does not exist, it will be created using the imported entries.
307
+
308
+ -f|--from DATE|MNEMONIC: The DATE or MNEMONIC that represents the start of the range of DSU dates to import. If a relative mnemonic is used (+/-n, e.g +1, -1, etc.), the date calculated will be relative to the current date (e.g. `<MNEMONIC>.to_i.days.from_now(Time.now)`).
309
+
310
+ -t|--to DATE|MNEMONIC: The DATE or MNEMONIC that represents the end of the range of DSU dates to import. If a relative mnemonic is used (+/-n, e.g +1, -1, etc.), the date calculated will be relative to the date that resulting from the `--from` option date calculation.
311
+
312
+ %{date_option_description}
313
+
314
+ %{mnemonic_option_description}
315
+
316
+ EXAMPLES:
317
+
318
+ NOTE: All examples can substitute their respective short form options (e.g. `-f`, `-t`, etc. for `--from`, `--to`, etc.).
319
+
320
+ The below will import the DSU entries for the range of dates from 1/1 to 1/4 for the current year, from the import file, and replace all the entries for the respective entry groups imported:
321
+
322
+ $ dsu import dates --from 1/1 --to +3 -i /path/to/import.csv -m false
323
+
324
+ This will import the DSU entries for the range of dates from 1/2 to 1/5 for the year 2022, from the import file, and merge all the entries for the respective entry groups imported:
325
+
326
+ $ dsu m dd --from 1/5/2022 --to -3 -i /path/to/import.csv
327
+
328
+ This (assuming "today" is 1/10) will import the DSU entries for the last week 1/10 to 1/3 of the current year, from the import file, and merge all the entries for the respective entry groups imported:
329
+
330
+ $ dsu import dates --from today --to -7 -i /path/to/import.csv -m true
331
+
332
+ This (assuming "today" is 5/23) will import the DSU entries for the last week 5/16 to 5/22.
333
+ This example simply illustrates the fact that you can use relative mnemonics for
334
+ both `--from` and `--to` options; this doesn't mean you should do so...
335
+
336
+ While you can use relative mnemonics for both `--from` and `--to` options,
337
+ there is always a more intuitive way. The below example basically imports one week of DSU entries back 1 week from yesterday's date, from the import file, and merge all the entries for the respective entry groups imported:
338
+
339
+ $ dsu import dates --from -7 --to +6 -i /path/to/import.csv
340
+
341
+ The above can be accomplished MUCH easier by simply using the `yesterday` mnemonic...
342
+
343
+ This (assuming "today" is 5/23) will import the DSU entries back 1 week from yesterday's date 5/16 to 5/22, from the import file, and merge all the entries for the respective entry groups imported:
344
+
345
+ $ dsu m dd --from yesterday --to -6 -i /path/to/import.csv
346
+ all:
347
+ desc: all|a OPTIONS
348
+ usage: Imports all DSU entries from a given DSU export .csv file
349
+ long_desc: |
350
+ Imports all DSU entries from a given DSU export .csv file.
351
+
352
+ $ dsu import all OPTIONS
353
+
354
+ $ dsu m a OPTIONS
355
+
356
+ OPTIONS:
357
+
358
+ -i|--import-file IMPORT_CVS_FILE: The IMPORT_CVS_FILE file to import. IMPORT_CVS_FILE should be a fully qualified path to a file that was previously created as a result of running `dsu export`. see `dsu help export`.
359
+
360
+ -m|--merge true|false (default: true): If true, imported entries will be added to the entry group if the entry group already exists. If false, the imported entries will replace all existing entries for the entry group if the entry group already exists. If the entry group does not exist, it will be created using the imported entries.
361
+
362
+ EXAMPLES:
363
+
364
+ This will import all the DSU entries from the import file, and replace all the entries for the respective entry groups imported:
365
+
366
+ $ dsu import all -i /path/to/import.csv -m false
367
+
368
+ This will import all the DSU entries from the import file, and merge all the entries for the respective entry groups imported:
369
+
370
+ $ dsu import all -i /path/to/import.csv
371
+ messages:
372
+ import_success: Entry group for %{date} imported successfully.
373
+ import_error: "Entry group for %{date} imported with an error: %{error}."
374
+ nothing_to_import: No entry groups to import.
375
+ cancelled: Cancelled.
376
+ file_not_exist: Import file %{file_path} does not exist.
377
+ prompts:
378
+ import_all_confirm: Import all entry groups (%{count} entry groups)?
379
+ import_dates_confirm: Import all the entry groups for %{from} thru %{to} (%{count} entry groups)?
380
+ options:
381
+ - y
382
+ - N
290
383
  list:
291
384
  date:
292
385
  desc: date|d DATE|MNEMONIC
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: 2.3.1
4
+ version: 2.4.0
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-12-26 00:00:00.000000000 Z
11
+ date: 2024-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -188,7 +188,13 @@ files:
188
188
  - lib/dsu/presenters/export/all_presenter.rb
189
189
  - lib/dsu/presenters/export/dates_presenter.rb
190
190
  - lib/dsu/presenters/export/messages.rb
191
+ - lib/dsu/presenters/export/nothing_to_export.rb
191
192
  - lib/dsu/presenters/export/service_callable.rb
193
+ - lib/dsu/presenters/import/all_presenter.rb
194
+ - lib/dsu/presenters/import/dates_presenter.rb
195
+ - lib/dsu/presenters/import/import_file.rb
196
+ - lib/dsu/presenters/import/messages.rb
197
+ - lib/dsu/presenters/import/service_callable.rb
192
198
  - lib/dsu/services/color_theme/hydrator_service.rb
193
199
  - lib/dsu/services/configuration/hydrator_service.rb
194
200
  - lib/dsu/services/entry/hydrator_service.rb
@@ -198,6 +204,7 @@ files:
198
204
  - lib/dsu/services/entry_group/editor_service.rb
199
205
  - lib/dsu/services/entry_group/exporter_service.rb
200
206
  - lib/dsu/services/entry_group/hydrator_service.rb
207
+ - lib/dsu/services/entry_group/importer_service.rb
201
208
  - lib/dsu/services/migration_version/hydrator_service.rb
202
209
  - lib/dsu/services/stderr_redirector_service.rb
203
210
  - lib/dsu/services/stdout_redirector_service.rb
@@ -209,6 +216,7 @@ files:
209
216
  - lib/dsu/subcommands/delete.rb
210
217
  - lib/dsu/subcommands/edit.rb
211
218
  - lib/dsu/subcommands/export.rb
219
+ - lib/dsu/subcommands/import.rb
212
220
  - lib/dsu/subcommands/list.rb
213
221
  - lib/dsu/subcommands/theme.rb
214
222
  - lib/dsu/support/ask.rb
@@ -245,6 +253,7 @@ files:
245
253
  - lib/dsu/views/entry_group/shared/no_entries_to_display_for_year_of.rb
246
254
  - lib/dsu/views/entry_group/show.rb
247
255
  - lib/dsu/views/export.rb
256
+ - lib/dsu/views/import.rb
248
257
  - lib/dsu/views/shared/error.rb
249
258
  - lib/dsu/views/shared/info.rb
250
259
  - lib/dsu/views/shared/message.rb
@@ -281,28 +290,25 @@ post_install_message: |
281
290
  View the dsu README.md here: https://github.com/gangelo/dsu
282
291
  View the dsu CHANGELOG.md: https://github.com/gangelo/dsu/blob/main/CHANGELOG.md
283
292
 
293
+ Dsu now has a import command! Try it out by running `dsu import help`.
284
294
  Dsu now has a export command! Try it out by running `dsu export help`.
285
295
  Dsu now has a browse command! Try it out by running `dsu browse help`.
286
-
287
- Dsu now has a festive "christmas" theme! Try it out by running `dsu theme use christmas`.
288
296
  Dsu now has a "light" theme for light background terminals! Try it out by running `dsu theme use light`.
289
-
290
297
  Dsu now has a delete command! Try it out by running `dsu delete help`.
291
298
 
292
299
  Try a dsu theme by running `dsu theme list` and then `dsu theme use THEME_NAME` where THEME_NAME is the name of the theme you want to try.
293
300
 
294
301
  :)
295
302
 
296
- Merry CHRISTmas, New Years and Happy holidays from dsu!
297
- *
298
- /*\
299
- */*|\*
300
- /*/|\*\
301
- */**|\*\*
302
- *//*|*\*\*\
303
- |||
304
- |||
305
- |||
303
+ Happy New Year 2024 from dsu!
304
+
305
+ * * *
306
+ * * . * . *
307
+ * * * * * *
308
+ * * * * *
309
+ * * *
310
+
311
+ May your year be filled with sparks of joy and innovation!
306
312
  rdoc_options: []
307
313
  require_paths:
308
314
  - lib