From b8ffaa847a2f0c83b5a4774c6a0954bf1bdb08b2 Mon Sep 17 00:00:00 2001 From: gangelo Date: Sat, 30 Dec 2023 21:13:09 -0500 Subject: [PATCH] Add dsu import command --- .gitignore | 2 + CHANGELOG.md | 6 ++ Gemfile.lock | 22 +---- lib/dsu/cli.rb | 5 + lib/dsu/models/color_theme.rb | 3 + lib/dsu/models/entry.rb | 3 + lib/dsu/presenters/import/all_presenter.rb | 68 ++++++++++++++ lib/dsu/presenters/import/dates_presenter.rb | 78 ++++++++++++++++ lib/dsu/presenters/import/import_file.rb | 25 +++++ lib/dsu/presenters/import/messages.rb | 47 ++++++++++ lib/dsu/presenters/import/service_callable.rb | 21 +++++ .../services/entry_group/importer_service.rb | 84 +++++++++++++++++ lib/dsu/subcommands/export.rb | 3 +- lib/dsu/subcommands/import.rb | 68 ++++++++++++++ lib/dsu/support/command_options/dsu_times.rb | 2 +- lib/dsu/validators/description_validator.rb | 21 ++++- lib/dsu/version.rb | 2 +- lib/dsu/views/import.rb | 29 ++++++ lib/locales/en/commands.yml | 4 + lib/locales/en/subcommands.yml | 92 +++++++++++++++++++ spec/dsu/features/dsu_delete_features_spec.rb | 6 +- spec/dsu/features/dsu_list_features_spec.rb | 10 +- spec/dsu/support/command_options/time_spec.rb | 2 +- spec/support/time_helpers.rb | 4 + 24 files changed, 573 insertions(+), 34 deletions(-) create mode 100644 lib/dsu/presenters/import/all_presenter.rb create mode 100644 lib/dsu/presenters/import/dates_presenter.rb create mode 100644 lib/dsu/presenters/import/import_file.rb create mode 100644 lib/dsu/presenters/import/messages.rb create mode 100644 lib/dsu/presenters/import/service_callable.rb create mode 100644 lib/dsu/services/entry_group/importer_service.rb create mode 100644 lib/dsu/subcommands/import.rb create mode 100644 lib/dsu/views/import.rb diff --git a/.gitignore b/.gitignore index fe2bea99..36da8f90 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,5 @@ todo.txt /spec/.tmp migration_version.yml + +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index 9de42e0d..7f6e0dc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [2.4.0] 2023-nn-nn + +Enhancements + +- Add `dsu import` command to import DSU entries from a comma-delimited csv file. See `dsu help import` for more information. + ## [2.3.2] 2023-12-30 Changes diff --git a/Gemfile.lock b/Gemfile.lock index eeb6a33f..0029d741 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - dsu (2.3.2) + dsu (2.4.0) activemodel (>= 7.0.8, < 8.0) activesupport (>= 7.0.8, < 8.0) colorize (>= 0.8.1, < 1.0) @@ -12,32 +12,22 @@ PATH GEM remote: https://rubygems.org/ specs: - activemodel (7.1.2) - activesupport (= 7.1.2) - activesupport (7.1.2) - base64 - bigdecimal + activemodel (7.0.8) + activesupport (= 7.0.8) + activesupport (7.0.8) concurrent-ruby (~> 1.0, >= 1.0.2) - connection_pool (>= 2.2.5) - drb i18n (>= 1.6, < 2) minitest (>= 5.1) - mutex_m tzinfo (~> 2.0) ast (2.4.2) - base64 (0.2.0) - bigdecimal (3.1.5) byebug (11.1.3) coderay (1.1.3) colorize (0.8.1) concurrent-ruby (1.2.2) - connection_pool (2.4.1) diff-lcs (1.5.0) docile (1.4.0) dotenv (2.8.1) - drb (2.2.0) - ruby2_keywords - factory_bot (6.4.2) + factory_bot (6.4.5) activesupport (>= 5.0.0) ffaker (2.23.0) i18n (1.14.1) @@ -47,7 +37,6 @@ GEM language_server-protocol (3.17.0.3) method_source (1.0.0) minitest (5.20.0) - mutex_m (0.2.0) os (1.1.4) parallel (1.24.0) parser (3.2.2.4) @@ -106,7 +95,6 @@ GEM rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) ruby-progressbar (1.13.0) - ruby2_keywords (0.0.5) simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) diff --git a/lib/dsu/cli.rb b/lib/dsu/cli.rb index cb334f62..76f631a6 100644 --- a/lib/dsu/cli.rb +++ b/lib/dsu/cli.rb @@ -8,6 +8,7 @@ require_relative 'subcommands/delete' require_relative 'subcommands/edit' require_relative 'subcommands/export' +require_relative 'subcommands/import' require_relative 'subcommands/list' require_relative 'subcommands/theme' @@ -21,6 +22,7 @@ class CLI < BaseCLI map I18n.t('commands.edit.key_mappings') => :edit map I18n.t('commands.export.key_mappings') => :export map I18n.t('commands.help.key_mappings') => :help + map I18n.t('commands.import.key_mappings') => :import map I18n.t('commands.info.key_mappings') => :info map I18n.t('commands.list.key_mappings') => :list map I18n.t('commands.theme.key_mappings') => :theme @@ -68,6 +70,9 @@ def add(description) desc I18n.t('commands.theme.desc'), I18n.t('commands.theme.usage') subcommand :theme, Subcommands::Theme + desc I18n.t('commands.import.desc'), I18n.t('commands.import.usage') + subcommand :import, Subcommands::Import + desc I18n.t('commands.info.desc'), I18n.t('commands.info.usage') def info configuration_version = Models::Configuration::VERSION diff --git a/lib/dsu/models/color_theme.rb b/lib/dsu/models/color_theme.rb index dbe855b4..fa82e650 100644 --- a/lib/dsu/models/color_theme.rb +++ b/lib/dsu/models/color_theme.rb @@ -54,6 +54,9 @@ class ColorTheme < Crud::JsonFile description: 'Default theme.' }.merge(DEFAULT_THEME_COLORS).freeze + MIN_DESCRIPTION_LENGTH = 2 + MAX_DESCRIPTION_LENGTH = 256 + # TODO: Validate other attrs. validates_with Validators::DescriptionValidator validates_with Validators::ColorThemeValidator diff --git a/lib/dsu/models/entry.rb b/lib/dsu/models/entry.rb index 99818994..27c766ee 100644 --- a/lib/dsu/models/entry.rb +++ b/lib/dsu/models/entry.rb @@ -14,6 +14,9 @@ class Entry include Support::Descriptable include Support::Presentable + MIN_DESCRIPTION_LENGTH = 2 + MAX_DESCRIPTION_LENGTH = 256 + validates_with Validators::DescriptionValidator attr_reader :description, :options diff --git a/lib/dsu/presenters/import/all_presenter.rb b/lib/dsu/presenters/import/all_presenter.rb new file mode 100644 index 00000000..425bcf89 --- /dev/null +++ b/lib/dsu/presenters/import/all_presenter.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative '../../models/entry_group' +require_relative '../../services/entry_group/importer_service' +require_relative '../../support/ask' +require_relative '../base_presenter_ex' +require_relative 'import_file' +require_relative 'messages' +require_relative 'service_callable' + +module Dsu + module Presenters + module Import + class AllPresenter < BasePresenterEx + include ImportFile + include Messages + include ServiceCallable + include Support::Ask + + def initialize(import_file_path:, options: {}) + super(options: options) + + @import_file_path = import_file_path + end + + def render(response:) + return display_cancelled_message unless response + + importer_service_call.tap do |import_results| + if import_results.values.all?(&:empty?) + display_import_success_message + else + display_import_error_message import_results + end + end + end + + def display_import_prompt + yes?(prompt_with_options(prompt: import_prompt, options: import_prompt_options), options: options) + end + + private + + attr_reader :import_file_path, :options + + def import_entry_groups + @import_entry_groups ||= CSV.foreach(import_file_path, + headers: true).with_object({}) do |entry_group_entry, entry_groups_hash| + next unless entry_group_entry['version'].to_i == Dsu::Migration::VERSION + + Date.parse(entry_group_entry['entry_group']).to_s.tap do |time| + entry_groups_hash[time] = [] unless entry_groups_hash.key?(time) + entry_groups_hash[time] << entry_group_entry['entry_group_entry'] + end + end + end + + def import_prompt + I18n.t('subcommands.import.prompts.import_all_confirm', count: import_entry_groups.count) + end + + def import_prompt_options + I18n.t('subcommands.import.prompts.options') + end + end + end + end +end diff --git a/lib/dsu/presenters/import/dates_presenter.rb b/lib/dsu/presenters/import/dates_presenter.rb new file mode 100644 index 00000000..4c2668d6 --- /dev/null +++ b/lib/dsu/presenters/import/dates_presenter.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require_relative '../../models/entry_group' +require_relative '../../services/entry_group/importer_service' +require_relative '../../support/ask' +require_relative '../base_presenter_ex' +require_relative 'import_file' +require_relative 'messages' +require_relative 'service_callable' + +module Dsu + module Presenters + module Import + class DatesPresenter < BasePresenterEx + include ImportFile + include Messages + include ServiceCallable + include Support::Ask + + def initialize(from:, to:, import_file_path:, options: {}) + super(options: options) + + @from = from.beginning_of_day + @to = to.end_of_day + @import_file_path = import_file_path + end + + def render(response:) + return display_cancelled_message unless response + + importer_service_call.tap do |import_results| + if import_results.values.all?(&:empty?) + display_import_success_message + else + display_import_error_message import_results + end + end + end + + def display_import_prompt + yes?(prompt_with_options(prompt: import_prompt, options: import_prompt_options), options: options) + end + + private + + attr_reader :from, :to, :import_file_path, :options + + def import_entry_groups + @import_entry_groups ||= CSV.foreach(import_file_path, + headers: true).with_object({}) do |entry_group_entry, entry_groups_hash| + next unless entry_group_entry['version'].to_i == Dsu::Migration::VERSION + + entry_group_time = middle_of_day_for(entry_group_entry['entry_group']) + next unless entry_group_time.to_date.between?(from.to_date, to.to_date) + + entry_group_time.to_date.to_s.tap do |time| + entry_groups_hash[time] = [] unless entry_groups_hash.key?(time) + entry_groups_hash[time] << entry_group_entry['entry_group_entry'] + end + end + end + + def import_prompt + I18n.t('subcommands.import.prompts.import_dates_confirm', + from: from.to_date, to: to.to_date, count: import_entry_groups.keys.count) + end + + def import_prompt_options + I18n.t('subcommands.import.prompts.options') + end + + def middle_of_day_for(date_string) + Time.parse(date_string).in_time_zone.middle_of_day + end + end + end + end +end diff --git a/lib/dsu/presenters/import/import_file.rb b/lib/dsu/presenters/import/import_file.rb new file mode 100644 index 00000000..232e4342 --- /dev/null +++ b/lib/dsu/presenters/import/import_file.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Dsu + module Presenters + module Import + module ImportFile + def import_file_path_exist? + File.exist? import_file_path + end + + def nothing_to_import? + return true unless import_file_path_exist? + + import_entry_groups.empty? + end + + def import_entry_groups + # Should return a Hash of entry group entries + # Example: { '2023-12-32' => ['Entry description 1', 'Entry description 2', ...] } + raise NotImplementedError + end + end + end + end +end diff --git a/lib/dsu/presenters/import/messages.rb b/lib/dsu/presenters/import/messages.rb new file mode 100644 index 00000000..ea97dcb4 --- /dev/null +++ b/lib/dsu/presenters/import/messages.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Dsu + module Presenters + module Import + module Messages + def display_import_prompt + raise NotImplementedError + end + + def display_import_file_not_exist_message + puts apply_theme(I18n.t('subcommands.import.messages.file_not_exist', + file_path: import_file_path), theme_color: color_theme.info) + end + + def display_nothing_to_import_message + puts apply_theme(I18n.t('subcommands.import.messages.nothing_to_import'), theme_color: color_theme.info) + end + + private + + def display_cancelled_message + puts apply_theme(I18n.t('subcommands.import.messages.cancelled'), theme_color: color_theme.info) + end + + def display_import_success_message + puts apply_theme(I18n.t('subcommands.import.messages.import_success'), + theme_color: color_theme.success) + end + + def display_import_error_message(import_results) + import_results.each_pair do |entry_group_date, errors| + if errors.empty? + puts apply_theme(I18n.t('subcommands.import.messages.import_success', + date: entry_group_date), theme_color: color_theme.success) + else + errors.each do |error| + puts apply_theme(I18n.t('subcommands.import.messages.import_error', + date: entry_group_date, error: error), theme_color: color_theme.error) + end + end + end + end + end + end + end +end diff --git a/lib/dsu/presenters/import/service_callable.rb b/lib/dsu/presenters/import/service_callable.rb new file mode 100644 index 00000000..d76c21bb --- /dev/null +++ b/lib/dsu/presenters/import/service_callable.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative '../../services/entry_group/importer_service' + +module Dsu + module Presenters + module Import + module ServiceCallable + private + + def importer_service_call + @importer_service_call ||= begin + importer_service = Services::EntryGroup::ImporterService.new(import_entry_groups: import_entry_groups, + options: options) + importer_service.call + end + end + end + end + end +end diff --git a/lib/dsu/services/entry_group/importer_service.rb b/lib/dsu/services/entry_group/importer_service.rb new file mode 100644 index 00000000..065f7f75 --- /dev/null +++ b/lib/dsu/services/entry_group/importer_service.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'csv' +require_relative '../../models/entry_group' + +module Dsu + module Services + module EntryGroup + # Expects a hash having the following format: + # { + # "2023-12-29" => ["Entry 1 description", "Entry 2 description", ...], + # "2023-12-30" => ["Entry 1 description", ...], + # "2023-12-31" => ["Entry 1 description", ...] + # } + class ImporterService + include Support::Fileable + + def initialize(import_entry_groups:, options: {}) + raise ArgumentError, 'Argument import_entry_groups is blank' if import_entry_groups.blank? + + @import_entry_groups = import_entry_groups + @options = options + end + + def call + import! + end + + private + + attr_reader :import_entry_groups, :options + + def import! + import_entry_groups.each_pair do |entry_group_date, entry_descriptions| + entry_group_for(entry_group_date).tap do |entry_group| + entry_descriptions.each do |entry_description| + add_entry_group_entry_if(entry_group: entry_group, entry_description: entry_description) + end + + import_messages[entry_group.time_yyyy_mm_dd] = [] + + unless entry_group.save + entry_group.errors.full_messages.each do |error| + import_messages[entry_group.time_yyyy_mm_dd] << error + end + end + end + end + + import_messages + end + + def entry_group_for(entry_group_date) + time = Time.parse(entry_group_date).in_time_zone + if merge? + Models::EntryGroup.find_or_initialize(time: time) + else + Models::EntryGroup.new(time: time, options: options) + end + end + + def add_entry_group_entry_if(entry_group:, entry_description:) + entry = Models::Entry.new(description: entry_description) + return entry_group.entries << entry if replace? + return if entry_group.entries.include?(entry) + + entry_group.entries << entry + end + + def merge? + options.fetch(:merge, true) + end + + def replace? + !merge? + end + + def import_messages + @import_messages ||= {} + end + end + end + end +end diff --git a/lib/dsu/subcommands/export.rb b/lib/dsu/subcommands/export.rb index 3be029f0..5a842961 100644 --- a/lib/dsu/subcommands/export.rb +++ b/lib/dsu/subcommands/export.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require_relative '../presenters/export/all_presenter' +require_relative '../presenters/export/dates_presenter' require_relative '../support/command_options/dsu_times' require_relative '../support/command_options/time_mnemonic' require_relative '../support/time_formatable' @@ -39,7 +41,6 @@ def dates return end - times = times_sort(times: times, entries_display_order: options[:entries_display_order]) Views::Export.new(presenter: dates_presenter_for(from: times.min, to: times.max, options: options)).render rescue ArgumentError => e Views::Shared::Error.new(messages: e.message).render diff --git a/lib/dsu/subcommands/import.rb b/lib/dsu/subcommands/import.rb new file mode 100644 index 00000000..fbaa0a07 --- /dev/null +++ b/lib/dsu/subcommands/import.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require_relative '../presenters/import/all_presenter' +require_relative '../presenters/import/dates_presenter' +require_relative '../support/command_options/dsu_times' +require_relative '../support/command_options/time_mnemonic' +require_relative '../support/time_formatable' +require_relative '../views/import' +require_relative '../views/shared/error' +require_relative 'base_subcommand' + +module Dsu + module Subcommands + class Import < BaseSubcommand + include Support::CommandOptions::TimeMnemonic + include Support::TimeFormatable + + # TODO: I18n. + map %w[a] => :all + map %w[dd] => :dates + + desc I18n.t('subcommands.import.all.desc'), I18n.t('subcommands.import.all.usage') + long_desc I18n.t('subcommands.import.all.long_desc') + option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE' + option :merge, type: :boolean, default: true, aliases: '-m' + option :prompts, type: :hash, default: {}, hide: true, aliases: '-p' + def all + Views::Import.new(presenter: all_presenter(import_file_path: options[:import_file], + options: options)).render + end + + desc I18n.t('subcommands.import.dates.desc'), I18n.t('subcommands.import.dates.usage') + long_desc I18n.t('subcommands.import.dates.long_desc', + date_option_description: date_option_description, + mnemonic_option_description: mnemonic_option_description) + option :from, type: :string, required: true, aliases: '-f', banner: 'DATE|MNEMONIC' + option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC' + option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE' + option :merge, type: :boolean, default: true, aliases: '-m' + option :prompts, type: :hash, default: {}, hide: true, aliases: '-p' + def dates + options = configuration.to_h.merge(self.options).with_indifferent_access + times, errors = Support::CommandOptions::DsuTimes.dsu_times_for(from_option: options[:from], to_option: options[:to]) # rubocop:disable Layout/LineLength + if errors.any? + Views::Shared::Error.new(messages: errors).render + return + end + + Views::Import.new(presenter: dates_presenter_for(from: times.min, + to: times.max, + import_file_path: options[:import_file], + options: options)).render + rescue ArgumentError => e + Views::Shared::Error.new(messages: e.message).render + end + + private + + def all_presenter(import_file_path:, options:) + Presenters::Import::AllPresenter.new(import_file_path: import_file_path, options: options) + end + + def dates_presenter_for(from:, to:, import_file_path:, options:) + Presenters::Import::DatesPresenter.new(from: from, to: to, import_file_path: import_file_path, options: options) + end + end + end +end diff --git a/lib/dsu/support/command_options/dsu_times.rb b/lib/dsu/support/command_options/dsu_times.rb index 18dfcbdc..021fea70 100644 --- a/lib/dsu/support/command_options/dsu_times.rb +++ b/lib/dsu/support/command_options/dsu_times.rb @@ -21,7 +21,7 @@ def dsu_times_for(from_option:, to_option:) errors << I18n.t('errors.to_option_invalid', to_option: to_option) if to_time.nil? return [[], errors] if errors.any? - min_time, max_time = [from_time, to_time].sort + min_time, max_time = [from_time, to_time].minmax [(min_time.to_date..max_time.to_date).map(&:to_time), []] end diff --git a/lib/dsu/validators/description_validator.rb b/lib/dsu/validators/description_validator.rb index 6d1ed370..303f3d2b 100644 --- a/lib/dsu/validators/description_validator.rb +++ b/lib/dsu/validators/description_validator.rb @@ -12,6 +12,7 @@ def validate(record) end unless description.is_a?(String) + # TODO: I18n. record.errors.add(:description, 'is the wrong object type. ' \ "\"String\" was expected, but \"#{description.class}\" was received.") return @@ -25,16 +26,26 @@ def validate(record) def validate_description(record) description = record.description - return if description.length.between?(2, 256) + return if description.length.between?(min_description_length(record), max_description_length(record)) - if description.length < 2 + if description.length < min_description_length(record) # TODO: I18n. - record.errors.add(:description, "is too short: \"#{record.short_description}\" (minimum is 2 characters).") - elsif description.length > 256 + record.errors.add(:description, "is too short: \"#{record.short_description}\" " \ + "(minimum is #{min_description_length(record)} characters).") + elsif description.length > max_description_length(record) # TODO: I18n. - record.errors.add(:description, "is too long: \"#{record.short_description}\" (maximum is 256 characters).") + record.errors.add(:description, "is too long: \"#{record.short_description}\" " \ + "(maximum is #{max_description_length(record)} characters).") end end + + def min_description_length(record) + record.class::MIN_DESCRIPTION_LENGTH + end + + def max_description_length(record) + record.class::MAX_DESCRIPTION_LENGTH + end end end end diff --git a/lib/dsu/version.rb b/lib/dsu/version.rb index a88a2578..b3f1aaa8 100644 --- a/lib/dsu/version.rb +++ b/lib/dsu/version.rb @@ -2,5 +2,5 @@ module Dsu VERSION_REGEX = /\A\d+\.\d+\.\d+(\.(alpha|rc)\.\d+)?\z/ - VERSION = '2.3.2' + VERSION = '2.4.0' end diff --git a/lib/dsu/views/import.rb b/lib/dsu/views/import.rb new file mode 100644 index 00000000..aa0db441 --- /dev/null +++ b/lib/dsu/views/import.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require_relative '../models/color_theme' +require_relative '../models/configuration' +require_relative '../support/color_themable' + +module Dsu + module Views + class Import + include Support::ColorThemable + + def initialize(presenter:) + @presenter = presenter + end + + def render + return presenter.display_import_file_not_exist_message unless presenter.import_file_path_exist? + return presenter.display_nothing_to_import_message if presenter.nothing_to_import? + + response = presenter.display_import_prompt + presenter.render response: response + end + + private + + attr_reader :presenter + end + end +end diff --git a/lib/locales/en/commands.yml b/lib/locales/en/commands.yml index 041acb00..2f060449 100644 --- a/lib/locales/en/commands.yml +++ b/lib/locales/en/commands.yml @@ -69,6 +69,10 @@ en: key_mappings: x desc: export|x SUBCOMMAND usage: Export DSU entries for the given SUBCOMMAND + import: + key_mappings: m + desc: import|m SUBCOMMAND + usage: Imports DSU entries for the given SUBCOMMAND info: key_mappings: i desc: info|i diff --git a/lib/locales/en/subcommands.yml b/lib/locales/en/subcommands.yml index 873ada13..073dcda7 100644 --- a/lib/locales/en/subcommands.yml +++ b/lib/locales/en/subcommands.yml @@ -288,6 +288,98 @@ en: options: - y - N + import: + dates: + desc: dates|dd OPTIONS + usage: Imports the DSU entries given the OPTIONS provided + long_desc: | + Imports the DSU entries for the given OPTIONS provided. + + $ dsu import dates OPTIONS + + $ dsu m dd OPTIONS + + OPTIONS: + + -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`. + + -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. + + -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. `.to_i.days.from_now(Time.now)`). + + -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. + + %{date_option_description} + + %{mnemonic_option_description} + + EXAMPLES: + + NOTE: All examples can substitute their respective short form options (e.g. `-f`, `-t`, etc. for `--from`, `--to`, etc.). + + 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: + + $ dsu import dates --from 1/1 --to +3 -i /path/to/import.csv -m false + + 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: + + $ dsu m dd --from 1/5/2022 --to -3 -i /path/to/import.csv + + 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: + + $ dsu import dates --from today --to -7 -i /path/to/import.csv -m true + + This (assuming "today" is 5/23) will import the DSU entries for the last week 5/16 to 5/22. + This example simply illustrates the fact that you can use relative mnemonics for + both `--from` and `--to` options; this doesn't mean you should do so... + + While you can use relative mnemonics for both `--from` and `--to` options, + 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: + + $ dsu import dates --from -7 --to +6 -i /path/to/import.csv + + The above can be accomplished MUCH easier by simply using the `yesterday` mnemonic... + + 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: + + $ dsu m dd --from yesterday --to -6 -i /path/to/import.csv + all: + desc: all|a OPTIONS + usage: Imports all DSU entries from a given DSU export .csv file + long_desc: | + Imports all DSU entries from a given DSU export .csv file. + + $ dsu import all OPTIONS + + $ dsu m a OPTIONS + + OPTIONS: + + -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`. + + -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. + + EXAMPLES: + + This will import all the DSU entries from the import file, and replace all the entries for the respective entry groups imported: + + $ dsu import all -i /path/to/import.csv -m false + + This will import all the DSU entries from the import file, and merge all the entries for the respective entry groups imported: + + $ dsu import all -i /path/to/import.csv + messages: + import_success: Entry group for %{date} imported successfully. + import_error: "Entry group for %{date} imported with an error: %{error}." + nothing_to_import: No entry groups to import. + cancelled: Cancelled. + file_not_exist: Import file %{file_path} does not exist. + prompts: + import_all_confirm: Import all entry groups (%{count} entry groups)? + import_dates_confirm: Import all the entry groups for %{from} thru %{to} (%{count} entry groups)? + options: + - y + - N list: date: desc: date|d DATE|MNEMONIC diff --git a/spec/dsu/features/dsu_delete_features_spec.rb b/spec/dsu/features/dsu_delete_features_spec.rb index 9f8d095d..1aecddcb 100644 --- a/spec/dsu/features/dsu_delete_features_spec.rb +++ b/spec/dsu/features/dsu_delete_features_spec.rb @@ -236,8 +236,8 @@ [ 'delete', 'dates', - '-f', Dsu::Support::TimeFormatable.mm_dd(time: times.min), - '-t', Dsu::Support::TimeFormatable.mm_dd(time: times.max), + '-f', Dsu::Support::TimeFormatable.mm_dd_yyyy(time: times.min), + '-t', Dsu::Support::TimeFormatable.mm_dd_yyyy(time: times.max), '--prompts', 'any:true' ] end @@ -266,7 +266,7 @@ [ 'delete', 'dates', - '-f', Dsu::Support::TimeFormatable.mm_dd(time: Time.now.yesterday), + '-f', Dsu::Support::TimeFormatable.mm_dd_yyyy(time: Time.now.yesterday), '-t', 'today', '--prompts', 'any:true' ] diff --git a/spec/dsu/features/dsu_list_features_spec.rb b/spec/dsu/features/dsu_list_features_spec.rb index a5280612..40bd1255 100644 --- a/spec/dsu/features/dsu_list_features_spec.rb +++ b/spec/dsu/features/dsu_list_features_spec.rb @@ -193,8 +193,8 @@ [ 'list', 'dates', - '-f', Dsu::Support::TimeFormatable.mm_dd(time: times.min), - '-t', Dsu::Support::TimeFormatable.mm_dd(time: times.max) + '-f', Dsu::Support::TimeFormatable.mm_dd_yyyy(time: times.min), + '-t', Dsu::Support::TimeFormatable.mm_dd_yyyy(time: times.max) ] end let(:times) { [Time.now.yesterday, Time.now] } @@ -221,7 +221,7 @@ [ 'list', 'dates', - '-f', Dsu::Support::TimeFormatable.mm_dd(time: Time.now.yesterday), + '-f', Dsu::Support::TimeFormatable.mm_dd_yyyy(time: Time.now.yesterday), '-t', 'today' ] end @@ -284,8 +284,8 @@ def view_entry_groups(times) end def dsu_times_for(times) - from = Dsu::Support::TimeFormatable.mm_dd(time: times.min) - to = Dsu::Support::TimeFormatable.mm_dd(time: times.max) + from = Dsu::Support::TimeFormatable.mm_dd_yyyy(time: times.min) + to = Dsu::Support::TimeFormatable.mm_dd_yyyy(time: times.max) times, errors = Dsu::Support::CommandOptions::DsuTimes.dsu_times_for(from_option: from, to_option: to) raise errors.join("\n") if errors.any? diff --git a/spec/dsu/support/command_options/time_spec.rb b/spec/dsu/support/command_options/time_spec.rb index 549cf7e2..e5f628c3 100644 --- a/spec/dsu/support/command_options/time_spec.rb +++ b/spec/dsu/support/command_options/time_spec.rb @@ -93,7 +93,7 @@ let(:command_option) { '2/1' } it 'returns a Time object' do - expect(to_yyyymmdd_string(time)).to eq('2023-02-01 EST') + expect(to_yyyymmdd_string(time)).to eq("#{current_year}-02-01 EST") end end end diff --git a/spec/support/time_helpers.rb b/spec/support/time_helpers.rb index 9dd75ca2..7b23582d 100644 --- a/spec/support/time_helpers.rb +++ b/spec/support/time_helpers.rb @@ -3,6 +3,10 @@ # This module provides methods to help with Time # objects module TimeHelpers + def current_year + Time.now.in_time_zone.year + end + def freeze_time_at(time_string:) allow(Time).to receive(:now).and_return(Time.parse(time_string)) end