doto 0.0.1.pre.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.env.test +1 -0
- data/.reek.yml +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +206 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +179 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/Rakefile +16 -0
- data/bin/console +36 -0
- data/bin/doto +3 -0
- data/bin/setup +18 -0
- data/exe/doto +33 -0
- data/lib/core/ruby/color_theme_colors.rb +16 -0
- data/lib/core/ruby/color_theme_mode.rb +42 -0
- data/lib/core/ruby/wrap_and_join.rb +31 -0
- data/lib/doto/base_cli.rb +56 -0
- data/lib/doto/cli.rb +131 -0
- data/lib/doto/command_services/add_entry_service.rb +50 -0
- data/lib/doto/crud/json_file.rb +161 -0
- data/lib/doto/env.rb +44 -0
- data/lib/doto/migration/base_service.rb +118 -0
- data/lib/doto/migration/migrator.rb +24 -0
- data/lib/doto/migration/raw_helpers/color_theme_hash.rb +13 -0
- data/lib/doto/migration/raw_helpers/configuration_hash.rb +15 -0
- data/lib/doto/migration/raw_helpers/entry_group_hash.rb +13 -0
- data/lib/doto/migration/raw_json_file.rb +15 -0
- data/lib/doto/migration/raw_json_files.rb +56 -0
- data/lib/doto/migration/v20230613121411/service.rb +94 -0
- data/lib/doto/migration/v20240210161248/service.rb +148 -0
- data/lib/doto/migration/version.rb +7 -0
- data/lib/doto/models/color_theme.rb +224 -0
- data/lib/doto/models/configuration.rb +185 -0
- data/lib/doto/models/entry.rb +63 -0
- data/lib/doto/models/entry_group.rb +223 -0
- data/lib/doto/models/migration_version.rb +49 -0
- data/lib/doto/models/project.rb +295 -0
- data/lib/doto/presenters/base_presenter.rb +32 -0
- data/lib/doto/presenters/base_presenter_ex.rb +15 -0
- data/lib/doto/presenters/color_theme_presenter.rb +52 -0
- data/lib/doto/presenters/color_theme_show_presenter.rb +55 -0
- data/lib/doto/presenters/configuration_presenter.rb +50 -0
- data/lib/doto/presenters/entry_group/list/date_presenter.rb +77 -0
- data/lib/doto/presenters/entry_group/list/dates_presenter.rb +60 -0
- data/lib/doto/presenters/entry_group/list/messages.rb +15 -0
- data/lib/doto/presenters/entry_group/list/nothing_to_list.rb +15 -0
- data/lib/doto/presenters/entry_group_presenter.rb +35 -0
- data/lib/doto/presenters/entry_presenter.rb +25 -0
- data/lib/doto/presenters/export/all_presenter.rb +44 -0
- data/lib/doto/presenters/export/dates_presenter.rb +55 -0
- data/lib/doto/presenters/import/all_presenter.rb +57 -0
- data/lib/doto/presenters/import/dates_presenter.rb +70 -0
- data/lib/doto/presenters/import/import_entry.rb +22 -0
- data/lib/doto/presenters/import/import_file.rb +33 -0
- data/lib/doto/presenters/project/create_presenter.rb +44 -0
- data/lib/doto/presenters/project/defaultable.rb +15 -0
- data/lib/doto/presenters/project/delete_by_number_presenter.rb +54 -0
- data/lib/doto/presenters/project/delete_presenter.rb +53 -0
- data/lib/doto/presenters/project/list_presenter.rb +24 -0
- data/lib/doto/presenters/project/rename_by_number_presenter.rb +63 -0
- data/lib/doto/presenters/project/rename_presenter.rb +57 -0
- data/lib/doto/presenters/project/use_by_number_presenter.rb +57 -0
- data/lib/doto/presenters/project/use_presenter.rb +56 -0
- data/lib/doto/services/color_theme/hydrator_service.rb +42 -0
- data/lib/doto/services/configuration/hydrator_service.rb +42 -0
- data/lib/doto/services/entry/hydrator_service.rb +33 -0
- data/lib/doto/services/entry_group/browse_service.rb +100 -0
- data/lib/doto/services/entry_group/counter_service.rb +32 -0
- data/lib/doto/services/entry_group/deleter_service.rb +35 -0
- data/lib/doto/services/entry_group/editor_service.rb +103 -0
- data/lib/doto/services/entry_group/exporter_service.rb +98 -0
- data/lib/doto/services/entry_group/hydrator_service.rb +37 -0
- data/lib/doto/services/entry_group/importer_service.rb +117 -0
- data/lib/doto/services/migration_version/hydrator_service.rb +36 -0
- data/lib/doto/services/project/hydrator_service.rb +40 -0
- data/lib/doto/services/project/rename_service.rb +70 -0
- data/lib/doto/services/stderr_redirector_service.rb +27 -0
- data/lib/doto/services/stdout_redirector_service.rb +27 -0
- data/lib/doto/services/temp_file/reader_service.rb +33 -0
- data/lib/doto/services/temp_file/writer_service.rb +35 -0
- data/lib/doto/subcommands/base_subcommand.rb +12 -0
- data/lib/doto/subcommands/browse.rb +49 -0
- data/lib/doto/subcommands/config.rb +81 -0
- data/lib/doto/subcommands/delete.rb +108 -0
- data/lib/doto/subcommands/edit.rb +48 -0
- data/lib/doto/subcommands/export.rb +62 -0
- data/lib/doto/subcommands/import.rb +72 -0
- data/lib/doto/subcommands/list.rb +95 -0
- data/lib/doto/subcommands/project.rb +146 -0
- data/lib/doto/subcommands/theme.rb +131 -0
- data/lib/doto/support/ask.rb +44 -0
- data/lib/doto/support/color_themable.rb +36 -0
- data/lib/doto/support/command_help_colorizeable.rb +34 -0
- data/lib/doto/support/command_hookable.rb +71 -0
- data/lib/doto/support/command_options/doto_times.rb +48 -0
- data/lib/doto/support/command_options/time.rb +84 -0
- data/lib/doto/support/command_options/time_mnemonic.rb +108 -0
- data/lib/doto/support/command_options/time_mnemonics.rb +16 -0
- data/lib/doto/support/descriptable.rb +29 -0
- data/lib/doto/support/entry_group_browsable.rb +104 -0
- data/lib/doto/support/field_errors.rb +11 -0
- data/lib/doto/support/fileable.rb +136 -0
- data/lib/doto/support/presentable.rb +11 -0
- data/lib/doto/support/project_file_system.rb +118 -0
- data/lib/doto/support/short_string.rb +24 -0
- data/lib/doto/support/time_comparable.rb +21 -0
- data/lib/doto/support/time_formatable.rb +65 -0
- data/lib/doto/support/times_sortable.rb +71 -0
- data/lib/doto/support/transform_project_name.rb +24 -0
- data/lib/doto/support/utils.rb +11 -0
- data/lib/doto/validators/color_theme_validator.rb +74 -0
- data/lib/doto/validators/description_validator.rb +51 -0
- data/lib/doto/validators/entries_validator.rb +77 -0
- data/lib/doto/validators/project_name_validator.rb +58 -0
- data/lib/doto/validators/time_validator.rb +25 -0
- data/lib/doto/validators/version_validator.rb +29 -0
- data/lib/doto/version.rb +6 -0
- data/lib/doto/views/base_list_view.rb +41 -0
- data/lib/doto/views/color_theme/index.rb +62 -0
- data/lib/doto/views/color_theme/show.rb +107 -0
- data/lib/doto/views/configuration/show.rb +41 -0
- data/lib/doto/views/entry_group/edit.rb +121 -0
- data/lib/doto/views/entry_group/list.rb +23 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display.rb +53 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_month_of.rb +32 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_week_of.rb +33 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_year_of.rb +33 -0
- data/lib/doto/views/entry_group/show.rb +63 -0
- data/lib/doto/views/export.rb +82 -0
- data/lib/doto/views/import.rb +105 -0
- data/lib/doto/views/import_dates.rb +17 -0
- data/lib/doto/views/project/create.rb +87 -0
- data/lib/doto/views/project/delete.rb +96 -0
- data/lib/doto/views/project/delete_by_number.rb +19 -0
- data/lib/doto/views/project/list.rb +115 -0
- data/lib/doto/views/project/rename.rb +98 -0
- data/lib/doto/views/project/rename_by_number.rb +21 -0
- data/lib/doto/views/project/use.rb +97 -0
- data/lib/doto/views/project/use_by_number.rb +19 -0
- data/lib/doto/views/shared/error.rb +17 -0
- data/lib/doto/views/shared/info.rb +17 -0
- data/lib/doto/views/shared/message.rb +85 -0
- data/lib/doto/views/shared/model_errors.rb +32 -0
- data/lib/doto/views/shared/success.rb +17 -0
- data/lib/doto/views/shared/warning.rb +17 -0
- data/lib/doto.rb +33 -0
- data/lib/locales/en/active_record.yml +17 -0
- data/lib/locales/en/commands.yml +165 -0
- data/lib/locales/en/miscellaneous.yml +29 -0
- data/lib/locales/en/presenters.yml +19 -0
- data/lib/locales/en/services.yml +14 -0
- data/lib/locales/en/subcommands.yml +786 -0
- data/lib/seed_data/0/.todo +5 -0
- data/lib/seed_data/20230613121411/.doto +8 -0
- data/lib/seed_data/20230613121411/doto/migration_version.json +3 -0
- data/lib/seed_data/20230613121411/doto/themes/cherry.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/christmas.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/default.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/lemon.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/light.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/matrix.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/whiteout.json +79 -0
- data/lib/seed_data/20240210161248/.doto +9 -0
- data/lib/seed_data/20240210161248/doto/current_project.json +4 -0
- data/lib/seed_data/20240210161248/doto/migration_version.json +3 -0
- data/lib/seed_data/20240210161248/doto/projects/default/project.json +5 -0
- data/lib/seed_data/20240210161248/doto/themes/cherry.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/christmas.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/default.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/lemon.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/light.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/matrix.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/whiteout.json +79 -0
- data/sig/dsu.rbs +4 -0
- metadata +406 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/project'
|
|
4
|
+
require_relative '../base_presenter_ex'
|
|
5
|
+
require_relative 'defaultable'
|
|
6
|
+
|
|
7
|
+
module Doto
|
|
8
|
+
module Presenters
|
|
9
|
+
module Project
|
|
10
|
+
class UseByNumberPresenter < BasePresenterEx
|
|
11
|
+
include Defaultable
|
|
12
|
+
|
|
13
|
+
attr_reader :project_number
|
|
14
|
+
|
|
15
|
+
delegate :project_name, to: :project, allow_nil: true
|
|
16
|
+
delegate :description, to: :project, prefix: true, allow_nil: true
|
|
17
|
+
|
|
18
|
+
def initialize(project_number:, options: {})
|
|
19
|
+
super(options: options)
|
|
20
|
+
|
|
21
|
+
raise ArgumentError, 'project_number is blank' if project_number.blank?
|
|
22
|
+
|
|
23
|
+
self.project_number = project_number
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def respond(response:)
|
|
27
|
+
return false unless response
|
|
28
|
+
|
|
29
|
+
project.default! if make_default? && project&.present?
|
|
30
|
+
project.use! if project&.present?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def already_current_project?
|
|
34
|
+
project&.current_project?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def project_does_not_exist?
|
|
38
|
+
!project&.exist?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def project_errors
|
|
42
|
+
return false unless project&.persisted?
|
|
43
|
+
|
|
44
|
+
project.errors.full_messages
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_writer :project_number
|
|
50
|
+
|
|
51
|
+
def project
|
|
52
|
+
@project ||= Models::Project.find_by_number(project_number: project_number)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/project'
|
|
4
|
+
require_relative '../base_presenter_ex'
|
|
5
|
+
require_relative 'defaultable'
|
|
6
|
+
|
|
7
|
+
module Doto
|
|
8
|
+
module Presenters
|
|
9
|
+
module Project
|
|
10
|
+
class UsePresenter < BasePresenterEx
|
|
11
|
+
include Defaultable
|
|
12
|
+
|
|
13
|
+
attr_reader :project_name
|
|
14
|
+
|
|
15
|
+
delegate :description, to: :project, prefix: true, allow_nil: true
|
|
16
|
+
|
|
17
|
+
def initialize(project_name:, options: {})
|
|
18
|
+
super(options: options)
|
|
19
|
+
|
|
20
|
+
raise ArgumentError, 'project_name is blank' if project_name.blank?
|
|
21
|
+
|
|
22
|
+
self.project_name = project_name
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def respond(response:)
|
|
26
|
+
return false unless response
|
|
27
|
+
|
|
28
|
+
project.default! if make_default? && project&.present?
|
|
29
|
+
project.use! if project&.present?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def already_current_project?
|
|
33
|
+
project&.current_project?
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def project_does_not_exist?
|
|
37
|
+
!project.exist?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def project_errors
|
|
41
|
+
return false unless project.persisted?
|
|
42
|
+
|
|
43
|
+
project.errors.full_messages
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_writer :project_name
|
|
49
|
+
|
|
50
|
+
def project
|
|
51
|
+
@project ||= Models::Project.find_or_initialize(project_name: project_name)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/color_theme'
|
|
4
|
+
|
|
5
|
+
module Doto
|
|
6
|
+
module Services
|
|
7
|
+
module ColorTheme
|
|
8
|
+
class HydratorService
|
|
9
|
+
def initialize(theme_name:, theme_hash:, options: {})
|
|
10
|
+
raise ArgumentError, 'theme_name is nil.' if theme_name.nil?
|
|
11
|
+
raise ArgumentError, "theme_name is the wrong object type: \"#{theme_name}\"." unless theme_hash.is_a?(Hash)
|
|
12
|
+
raise ArgumentError, 'theme_hash is nil' if theme_hash.nil?
|
|
13
|
+
raise ArgumentError, "theme_hash is the wrong object type: \"#{theme_hash}\"" unless theme_hash.is_a?(Hash)
|
|
14
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
|
15
|
+
raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
@theme_name = theme_name
|
|
18
|
+
@theme_hash = theme_hash
|
|
19
|
+
@options = options || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
Models::ColorTheme.new(theme_name: theme_name, theme_hash: hydrate)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :theme_hash, :theme_name, :options
|
|
29
|
+
|
|
30
|
+
def hydrate
|
|
31
|
+
theme_hash.each_pair do |key, value|
|
|
32
|
+
next if %i[version description].include?(key)
|
|
33
|
+
|
|
34
|
+
value.each_pair do |k, _v|
|
|
35
|
+
value[k] = value[k].to_sym
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/configuration'
|
|
4
|
+
require_relative '../../views/shared/message'
|
|
5
|
+
|
|
6
|
+
module Doto
|
|
7
|
+
module Services
|
|
8
|
+
module Configuration
|
|
9
|
+
class HydratorService
|
|
10
|
+
def initialize(config_hash:, options: {})
|
|
11
|
+
raise ArgumentError, 'config_hash is nil' if config_hash.nil?
|
|
12
|
+
|
|
13
|
+
unless config_hash.is_a?(Hash)
|
|
14
|
+
raise ArgumentError,
|
|
15
|
+
"config_hash is the wrong object type: \"#{config_hash}\""
|
|
16
|
+
end
|
|
17
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
|
18
|
+
raise ArgumentError, "options is the wrong object type:\"#{options}\"" unless options.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
@config_hash = config_hash.dup
|
|
21
|
+
@options = options || {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def call
|
|
25
|
+
hydrate
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
attr_reader :config_hash, :options
|
|
31
|
+
|
|
32
|
+
def hydrate
|
|
33
|
+
config_hash[:version] = config_hash[:version].to_i
|
|
34
|
+
config_hash[:entries_display_order] = config_hash[:entries_display_order].to_sym
|
|
35
|
+
config_hash
|
|
36
|
+
rescue JSON::ParserError => _e
|
|
37
|
+
Models::Configuration::DEFAULT_CONFIGURATION
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/entry'
|
|
4
|
+
|
|
5
|
+
module Doto
|
|
6
|
+
module Services
|
|
7
|
+
module Entry
|
|
8
|
+
class HydratorService
|
|
9
|
+
def initialize(entries_array:, options: {})
|
|
10
|
+
raise ArgumentError, 'entries_array is nil' if entries_array.nil?
|
|
11
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
|
12
|
+
|
|
13
|
+
@entries_array = entries_array
|
|
14
|
+
@options = options || {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
hydrate
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :entries_array, :options
|
|
24
|
+
|
|
25
|
+
def hydrate
|
|
26
|
+
entries_array.map do |entry_hash|
|
|
27
|
+
Doto::Models::Entry.new(**entry_hash)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/entry_group'
|
|
4
|
+
require_relative '../../support/time_formatable'
|
|
5
|
+
require_relative '../../support/times_sortable'
|
|
6
|
+
|
|
7
|
+
module Doto
|
|
8
|
+
module Services
|
|
9
|
+
module EntryGroup
|
|
10
|
+
# This service is responsible for returning an array of
|
|
11
|
+
# sorted entry group dates as Time objects. The Time objects
|
|
12
|
+
# returned, and the sort order are determined by the options
|
|
13
|
+
# passed in.
|
|
14
|
+
class BrowseService
|
|
15
|
+
include Support::TimeFormatable
|
|
16
|
+
include Support::TimesSortable
|
|
17
|
+
|
|
18
|
+
def initialize(time:, options: {})
|
|
19
|
+
raise ArgumentError, 'Argument time is nil' if time.nil?
|
|
20
|
+
raise ArgumentError, 'Argument options is nil' if options.nil?
|
|
21
|
+
|
|
22
|
+
@time = time
|
|
23
|
+
@options = options
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def call
|
|
27
|
+
return [] if entry_group_times.empty?
|
|
28
|
+
|
|
29
|
+
times_sort(times: entry_group_times, entries_display_order: entries_display_order)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :time, :options
|
|
35
|
+
|
|
36
|
+
def entry_group_times
|
|
37
|
+
@entry_group_times ||= (min_time.to_i..max_time.to_i).step(1.day).each_with_object([]) do |time_step, times|
|
|
38
|
+
time = Time.at(time_step)
|
|
39
|
+
next unless include_all? || entry_group_count(time).positive?
|
|
40
|
+
|
|
41
|
+
times << time
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def entry_group_count(time)
|
|
46
|
+
entry_group = Models::EntryGroup.find_or_initialize(time: time)
|
|
47
|
+
entry_group.persisted? ? entry_group.entries.count : 0
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def min_time
|
|
51
|
+
@min_time ||= if week?
|
|
52
|
+
time.beginning_of_week
|
|
53
|
+
elsif month?
|
|
54
|
+
time.beginning_of_month
|
|
55
|
+
elsif year?
|
|
56
|
+
time.beginning_of_year
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def max_time
|
|
61
|
+
@max_time ||= if week?
|
|
62
|
+
time.end_of_week
|
|
63
|
+
elsif month?
|
|
64
|
+
time.end_of_month
|
|
65
|
+
elsif year?
|
|
66
|
+
time.end_of_year
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def entries_display_order
|
|
71
|
+
options[:entries_display_order] || default_entries_display_order
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default_entries_display_order
|
|
75
|
+
:asc
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def week?
|
|
79
|
+
options.fetch(:browse, default_browse) == :week
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def month?
|
|
83
|
+
options[:browse] == :month
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def year?
|
|
87
|
+
options[:browse] == :year
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def default_browse
|
|
91
|
+
:week
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def include_all?
|
|
95
|
+
options.fetch(:include_all, false)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/entry_group'
|
|
4
|
+
|
|
5
|
+
module Doto
|
|
6
|
+
module Services
|
|
7
|
+
module EntryGroup
|
|
8
|
+
class CounterService
|
|
9
|
+
def initialize(times:, options: {})
|
|
10
|
+
raise ArgumentError, 'Argument times is nil' if times.nil?
|
|
11
|
+
|
|
12
|
+
@times = times
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
total_entry_groups = 0
|
|
18
|
+
|
|
19
|
+
times.each do |time|
|
|
20
|
+
total_entry_groups += 1 if Models::EntryGroup.exist?(time: time)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
total_entry_groups
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :times, :options
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/entry_group'
|
|
4
|
+
|
|
5
|
+
module Doto
|
|
6
|
+
module Services
|
|
7
|
+
module EntryGroup
|
|
8
|
+
class DeleterService
|
|
9
|
+
def initialize(times:, options: {})
|
|
10
|
+
raise ArgumentError, 'Argument times is nil' if times.nil?
|
|
11
|
+
|
|
12
|
+
@times = times
|
|
13
|
+
@options = options
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
deleted_entry_groups = 0
|
|
18
|
+
|
|
19
|
+
times.each do |time|
|
|
20
|
+
next unless Models::EntryGroup.exist?(time: time)
|
|
21
|
+
|
|
22
|
+
Models::EntryGroup.delete(time: time)
|
|
23
|
+
deleted_entry_groups += 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
deleted_entry_groups
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :times, :options
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/color_theme'
|
|
4
|
+
require_relative '../../models/entry'
|
|
5
|
+
require_relative '../../support/color_themable'
|
|
6
|
+
require_relative '../../support/time_formatable'
|
|
7
|
+
require_relative '../../views/shared/model_errors'
|
|
8
|
+
require_relative '../stdout_redirector_service'
|
|
9
|
+
require_relative '../temp_file/reader_service'
|
|
10
|
+
require_relative '../temp_file/writer_service'
|
|
11
|
+
|
|
12
|
+
module Doto
|
|
13
|
+
module Services
|
|
14
|
+
module EntryGroup
|
|
15
|
+
class EditorService
|
|
16
|
+
include Support::ColorThemable
|
|
17
|
+
include Support::TimeFormatable
|
|
18
|
+
|
|
19
|
+
def initialize(entry_group:, options: {})
|
|
20
|
+
raise ArgumentError, 'entry_group is nil' if entry_group.nil?
|
|
21
|
+
raise ArgumentError, 'entry_group is the wrong object type' unless entry_group.is_a?(Models::EntryGroup)
|
|
22
|
+
raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash) || options.nil?
|
|
23
|
+
|
|
24
|
+
@entry_group = entry_group
|
|
25
|
+
@options = options || {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def call
|
|
29
|
+
edit_view = render_edit_view
|
|
30
|
+
edit edit_view
|
|
31
|
+
# NOTE: Return the original entry group object as any permanent changes
|
|
32
|
+
# will have been applied to it.
|
|
33
|
+
entry_group
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
attr_reader :entry_group, :options
|
|
39
|
+
|
|
40
|
+
# Renders the edit view to a string so we can write it to a temporary file
|
|
41
|
+
# and edit it. The edits will be used to update the entry group.
|
|
42
|
+
def render_edit_view
|
|
43
|
+
message = I18n.t('services.editor_service.messages.editing',
|
|
44
|
+
formatted_time: formatted_time(time: entry_group.time))
|
|
45
|
+
puts apply_theme(message, theme_color: color_theme.header)
|
|
46
|
+
# TODO: Call #render_as_string directly below?
|
|
47
|
+
StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
|
|
48
|
+
# Views::EntryGroup::Edit.new(entry_group: entry_group).render_as_string
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Writes the temporary file contents to disk and opens it in the editor
|
|
52
|
+
# for editing. It then copies the changes to the entry group and writes
|
|
53
|
+
# the changes to the entry group file.
|
|
54
|
+
def edit(edit_view)
|
|
55
|
+
entry_group_with_edits = Models::EntryGroup.new(time: entry_group.time)
|
|
56
|
+
|
|
57
|
+
TempFile::WriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
|
|
58
|
+
if Kernel.system("${EDITOR:-#{configuration.editor}} #{tmp_file_path}")
|
|
59
|
+
TempFile::ReaderService.new(tmp_file_path: tmp_file_path).call do |editor_line|
|
|
60
|
+
next unless process_description?(editor_line)
|
|
61
|
+
|
|
62
|
+
entry_group_with_edits.entries << Models::Entry.new(description: editor_line)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
process_entry_group!(entry_group_with_edits)
|
|
66
|
+
else
|
|
67
|
+
message_array = I18n.t('services.editor_service.errors.temp_file_error',
|
|
68
|
+
editor: ENV.fetch('EDITOR', configuration.editor),
|
|
69
|
+
status: $CHILD_STATUS).split("\n")
|
|
70
|
+
puts apply_theme(message_array, theme_color: color_theme.error)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def process_entry_group!(entry_group_with_edits)
|
|
76
|
+
if entry_group_with_edits.entries.empty?
|
|
77
|
+
entry_group.delete
|
|
78
|
+
return
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
Views::Shared::ModelErrors.new(model: entry_group_with_edits).render if entry_group_with_edits.invalid?
|
|
82
|
+
|
|
83
|
+
# Make sure we're saving only valid, unique entries.
|
|
84
|
+
entry_group.entries = entry_group_with_edits.valid_unique_entries
|
|
85
|
+
entry_group.save!
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def process_description?(description)
|
|
89
|
+
description = Models::Entry.clean_description(description)
|
|
90
|
+
!(description.blank? || description[0] == '#')
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def color_theme
|
|
94
|
+
@color_theme ||= Models::ColorTheme.current_or_default
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def configuration
|
|
98
|
+
Models::Configuration.new
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
require_relative '../../models/entry_group'
|
|
5
|
+
require_relative '../../support/fileable'
|
|
6
|
+
require_relative '../../support/transform_project_name'
|
|
7
|
+
|
|
8
|
+
module Doto
|
|
9
|
+
module Services
|
|
10
|
+
module EntryGroup
|
|
11
|
+
# ExporterService exports entry groups to a CSV file.
|
|
12
|
+
# NOTE: This class exports all entries passed to it. It does not filter
|
|
13
|
+
# entries based on optional options[:time] that may be passed to it.
|
|
14
|
+
# Rather, times are used to determine the export file name only.
|
|
15
|
+
class ExporterService
|
|
16
|
+
include Support::Fileable
|
|
17
|
+
include Support::TransformProjectName
|
|
18
|
+
|
|
19
|
+
def initialize(project_name:, entry_groups:, options: {})
|
|
20
|
+
raise ArgumentError, 'Argument project_name is blank' if project_name.blank?
|
|
21
|
+
raise ArgumentError, 'Argument entry_groups is blank' if entry_groups.blank?
|
|
22
|
+
raise ArgumentError, 'Argument entry_groups are not all valid' unless entry_groups.all?(&:valid?)
|
|
23
|
+
|
|
24
|
+
validate_entry_group_entries_present! entry_groups
|
|
25
|
+
|
|
26
|
+
@project_name = project_name
|
|
27
|
+
@entry_groups = entry_groups
|
|
28
|
+
@options = options
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def call
|
|
32
|
+
CSV.open(export_file_path, 'w') do |csv|
|
|
33
|
+
csv << %i[project_name version entry_group entry_no total_entries entry_group_entry]
|
|
34
|
+
|
|
35
|
+
entry_groups.each do |entry_group|
|
|
36
|
+
export_entry_group(entry_group: entry_group, csv: csv)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
export_file_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def export_file_path
|
|
44
|
+
@export_file_path ||= File.join(temp_folder, export_file_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :project_name, :entry_groups, :options
|
|
50
|
+
|
|
51
|
+
def export_entry_data(entry_group:, entry:, entry_index:)
|
|
52
|
+
[
|
|
53
|
+
project_name,
|
|
54
|
+
entry_group.version,
|
|
55
|
+
entry_group.time.to_date,
|
|
56
|
+
entry_index + 1,
|
|
57
|
+
entry_group.entries.count,
|
|
58
|
+
entry.description
|
|
59
|
+
]
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def export_entry_group(entry_group:, csv:)
|
|
63
|
+
entry_group.entries.each_with_index do |entry, index|
|
|
64
|
+
csv << export_entry_data(entry_group: entry_group, entry: entry, entry_index: index)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def export_file_name
|
|
69
|
+
transformed_file_name = transform_project_name project_name, options: options
|
|
70
|
+
"doto-export-#{transformed_file_name}-" \
|
|
71
|
+
"#{timestamp}-#{export_scope}-#{times.min.to_date}-thru-#{times.max.to_date}.csv"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def export_scope
|
|
75
|
+
return 'all-entry-groups' unless options.fetch(:times, nil)
|
|
76
|
+
|
|
77
|
+
'entry-groups'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def times
|
|
81
|
+
@times ||= options.fetch(:times, entry_groups.map(&:time))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def timestamp
|
|
85
|
+
@timestamp ||= Time.now.in_time_zone.strftime('%Y%m%d%H%M%S')
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def validate_entry_group_entries_present!(entry_groups)
|
|
89
|
+
entry_groups.each do |entry_group|
|
|
90
|
+
next if entry_group.entries.present?
|
|
91
|
+
|
|
92
|
+
raise ArgumentError, "Argument entry_groups entry group for #{entry_group.time_yyyy_mm_dd} has no entries"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../../models/entry_group'
|
|
4
|
+
require_relative '../entry/hydrator_service'
|
|
5
|
+
|
|
6
|
+
module Doto
|
|
7
|
+
module Services
|
|
8
|
+
module EntryGroup
|
|
9
|
+
class HydratorService
|
|
10
|
+
def initialize(entry_group_hash:, options: {})
|
|
11
|
+
raise ArgumentError, 'entry_group_hash is nil' if entry_group_hash.nil?
|
|
12
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
|
13
|
+
|
|
14
|
+
@entry_group_hash = entry_group_hash
|
|
15
|
+
@options = options || {}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def call
|
|
19
|
+
Models::EntryGroup.new(**hydrate)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :entry_group_hash, :options
|
|
25
|
+
|
|
26
|
+
# Returns a Hash with :time and :entries values hydrated (i.e. Time and Entry objects respectively).
|
|
27
|
+
def hydrate
|
|
28
|
+
entry_group_hash.tap do |hash|
|
|
29
|
+
hash[:time] = Time.parse(hash[:time])
|
|
30
|
+
hash[:entries] =
|
|
31
|
+
Entry::HydratorService.new(entries_array: hash[:entries], options: options).call
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|