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.
Files changed (179) hide show
  1. checksums.yaml +7 -0
  2. data/.env.test +1 -0
  3. data/.reek.yml +20 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +206 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +7 -0
  8. data/CODE_OF_CONDUCT.md +84 -0
  9. data/Gemfile +30 -0
  10. data/Gemfile.lock +179 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +38 -0
  13. data/Rakefile +16 -0
  14. data/bin/console +36 -0
  15. data/bin/doto +3 -0
  16. data/bin/setup +18 -0
  17. data/exe/doto +33 -0
  18. data/lib/core/ruby/color_theme_colors.rb +16 -0
  19. data/lib/core/ruby/color_theme_mode.rb +42 -0
  20. data/lib/core/ruby/wrap_and_join.rb +31 -0
  21. data/lib/doto/base_cli.rb +56 -0
  22. data/lib/doto/cli.rb +131 -0
  23. data/lib/doto/command_services/add_entry_service.rb +50 -0
  24. data/lib/doto/crud/json_file.rb +161 -0
  25. data/lib/doto/env.rb +44 -0
  26. data/lib/doto/migration/base_service.rb +118 -0
  27. data/lib/doto/migration/migrator.rb +24 -0
  28. data/lib/doto/migration/raw_helpers/color_theme_hash.rb +13 -0
  29. data/lib/doto/migration/raw_helpers/configuration_hash.rb +15 -0
  30. data/lib/doto/migration/raw_helpers/entry_group_hash.rb +13 -0
  31. data/lib/doto/migration/raw_json_file.rb +15 -0
  32. data/lib/doto/migration/raw_json_files.rb +56 -0
  33. data/lib/doto/migration/v20230613121411/service.rb +94 -0
  34. data/lib/doto/migration/v20240210161248/service.rb +148 -0
  35. data/lib/doto/migration/version.rb +7 -0
  36. data/lib/doto/models/color_theme.rb +224 -0
  37. data/lib/doto/models/configuration.rb +185 -0
  38. data/lib/doto/models/entry.rb +63 -0
  39. data/lib/doto/models/entry_group.rb +223 -0
  40. data/lib/doto/models/migration_version.rb +49 -0
  41. data/lib/doto/models/project.rb +295 -0
  42. data/lib/doto/presenters/base_presenter.rb +32 -0
  43. data/lib/doto/presenters/base_presenter_ex.rb +15 -0
  44. data/lib/doto/presenters/color_theme_presenter.rb +52 -0
  45. data/lib/doto/presenters/color_theme_show_presenter.rb +55 -0
  46. data/lib/doto/presenters/configuration_presenter.rb +50 -0
  47. data/lib/doto/presenters/entry_group/list/date_presenter.rb +77 -0
  48. data/lib/doto/presenters/entry_group/list/dates_presenter.rb +60 -0
  49. data/lib/doto/presenters/entry_group/list/messages.rb +15 -0
  50. data/lib/doto/presenters/entry_group/list/nothing_to_list.rb +15 -0
  51. data/lib/doto/presenters/entry_group_presenter.rb +35 -0
  52. data/lib/doto/presenters/entry_presenter.rb +25 -0
  53. data/lib/doto/presenters/export/all_presenter.rb +44 -0
  54. data/lib/doto/presenters/export/dates_presenter.rb +55 -0
  55. data/lib/doto/presenters/import/all_presenter.rb +57 -0
  56. data/lib/doto/presenters/import/dates_presenter.rb +70 -0
  57. data/lib/doto/presenters/import/import_entry.rb +22 -0
  58. data/lib/doto/presenters/import/import_file.rb +33 -0
  59. data/lib/doto/presenters/project/create_presenter.rb +44 -0
  60. data/lib/doto/presenters/project/defaultable.rb +15 -0
  61. data/lib/doto/presenters/project/delete_by_number_presenter.rb +54 -0
  62. data/lib/doto/presenters/project/delete_presenter.rb +53 -0
  63. data/lib/doto/presenters/project/list_presenter.rb +24 -0
  64. data/lib/doto/presenters/project/rename_by_number_presenter.rb +63 -0
  65. data/lib/doto/presenters/project/rename_presenter.rb +57 -0
  66. data/lib/doto/presenters/project/use_by_number_presenter.rb +57 -0
  67. data/lib/doto/presenters/project/use_presenter.rb +56 -0
  68. data/lib/doto/services/color_theme/hydrator_service.rb +42 -0
  69. data/lib/doto/services/configuration/hydrator_service.rb +42 -0
  70. data/lib/doto/services/entry/hydrator_service.rb +33 -0
  71. data/lib/doto/services/entry_group/browse_service.rb +100 -0
  72. data/lib/doto/services/entry_group/counter_service.rb +32 -0
  73. data/lib/doto/services/entry_group/deleter_service.rb +35 -0
  74. data/lib/doto/services/entry_group/editor_service.rb +103 -0
  75. data/lib/doto/services/entry_group/exporter_service.rb +98 -0
  76. data/lib/doto/services/entry_group/hydrator_service.rb +37 -0
  77. data/lib/doto/services/entry_group/importer_service.rb +117 -0
  78. data/lib/doto/services/migration_version/hydrator_service.rb +36 -0
  79. data/lib/doto/services/project/hydrator_service.rb +40 -0
  80. data/lib/doto/services/project/rename_service.rb +70 -0
  81. data/lib/doto/services/stderr_redirector_service.rb +27 -0
  82. data/lib/doto/services/stdout_redirector_service.rb +27 -0
  83. data/lib/doto/services/temp_file/reader_service.rb +33 -0
  84. data/lib/doto/services/temp_file/writer_service.rb +35 -0
  85. data/lib/doto/subcommands/base_subcommand.rb +12 -0
  86. data/lib/doto/subcommands/browse.rb +49 -0
  87. data/lib/doto/subcommands/config.rb +81 -0
  88. data/lib/doto/subcommands/delete.rb +108 -0
  89. data/lib/doto/subcommands/edit.rb +48 -0
  90. data/lib/doto/subcommands/export.rb +62 -0
  91. data/lib/doto/subcommands/import.rb +72 -0
  92. data/lib/doto/subcommands/list.rb +95 -0
  93. data/lib/doto/subcommands/project.rb +146 -0
  94. data/lib/doto/subcommands/theme.rb +131 -0
  95. data/lib/doto/support/ask.rb +44 -0
  96. data/lib/doto/support/color_themable.rb +36 -0
  97. data/lib/doto/support/command_help_colorizeable.rb +34 -0
  98. data/lib/doto/support/command_hookable.rb +71 -0
  99. data/lib/doto/support/command_options/doto_times.rb +48 -0
  100. data/lib/doto/support/command_options/time.rb +84 -0
  101. data/lib/doto/support/command_options/time_mnemonic.rb +108 -0
  102. data/lib/doto/support/command_options/time_mnemonics.rb +16 -0
  103. data/lib/doto/support/descriptable.rb +29 -0
  104. data/lib/doto/support/entry_group_browsable.rb +104 -0
  105. data/lib/doto/support/field_errors.rb +11 -0
  106. data/lib/doto/support/fileable.rb +136 -0
  107. data/lib/doto/support/presentable.rb +11 -0
  108. data/lib/doto/support/project_file_system.rb +118 -0
  109. data/lib/doto/support/short_string.rb +24 -0
  110. data/lib/doto/support/time_comparable.rb +21 -0
  111. data/lib/doto/support/time_formatable.rb +65 -0
  112. data/lib/doto/support/times_sortable.rb +71 -0
  113. data/lib/doto/support/transform_project_name.rb +24 -0
  114. data/lib/doto/support/utils.rb +11 -0
  115. data/lib/doto/validators/color_theme_validator.rb +74 -0
  116. data/lib/doto/validators/description_validator.rb +51 -0
  117. data/lib/doto/validators/entries_validator.rb +77 -0
  118. data/lib/doto/validators/project_name_validator.rb +58 -0
  119. data/lib/doto/validators/time_validator.rb +25 -0
  120. data/lib/doto/validators/version_validator.rb +29 -0
  121. data/lib/doto/version.rb +6 -0
  122. data/lib/doto/views/base_list_view.rb +41 -0
  123. data/lib/doto/views/color_theme/index.rb +62 -0
  124. data/lib/doto/views/color_theme/show.rb +107 -0
  125. data/lib/doto/views/configuration/show.rb +41 -0
  126. data/lib/doto/views/entry_group/edit.rb +121 -0
  127. data/lib/doto/views/entry_group/list.rb +23 -0
  128. data/lib/doto/views/entry_group/shared/no_entries_to_display.rb +53 -0
  129. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_month_of.rb +32 -0
  130. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_week_of.rb +33 -0
  131. data/lib/doto/views/entry_group/shared/no_entries_to_display_for_year_of.rb +33 -0
  132. data/lib/doto/views/entry_group/show.rb +63 -0
  133. data/lib/doto/views/export.rb +82 -0
  134. data/lib/doto/views/import.rb +105 -0
  135. data/lib/doto/views/import_dates.rb +17 -0
  136. data/lib/doto/views/project/create.rb +87 -0
  137. data/lib/doto/views/project/delete.rb +96 -0
  138. data/lib/doto/views/project/delete_by_number.rb +19 -0
  139. data/lib/doto/views/project/list.rb +115 -0
  140. data/lib/doto/views/project/rename.rb +98 -0
  141. data/lib/doto/views/project/rename_by_number.rb +21 -0
  142. data/lib/doto/views/project/use.rb +97 -0
  143. data/lib/doto/views/project/use_by_number.rb +19 -0
  144. data/lib/doto/views/shared/error.rb +17 -0
  145. data/lib/doto/views/shared/info.rb +17 -0
  146. data/lib/doto/views/shared/message.rb +85 -0
  147. data/lib/doto/views/shared/model_errors.rb +32 -0
  148. data/lib/doto/views/shared/success.rb +17 -0
  149. data/lib/doto/views/shared/warning.rb +17 -0
  150. data/lib/doto.rb +33 -0
  151. data/lib/locales/en/active_record.yml +17 -0
  152. data/lib/locales/en/commands.yml +165 -0
  153. data/lib/locales/en/miscellaneous.yml +29 -0
  154. data/lib/locales/en/presenters.yml +19 -0
  155. data/lib/locales/en/services.yml +14 -0
  156. data/lib/locales/en/subcommands.yml +786 -0
  157. data/lib/seed_data/0/.todo +5 -0
  158. data/lib/seed_data/20230613121411/.doto +8 -0
  159. data/lib/seed_data/20230613121411/doto/migration_version.json +3 -0
  160. data/lib/seed_data/20230613121411/doto/themes/cherry.json +79 -0
  161. data/lib/seed_data/20230613121411/doto/themes/christmas.json +79 -0
  162. data/lib/seed_data/20230613121411/doto/themes/default.json +79 -0
  163. data/lib/seed_data/20230613121411/doto/themes/lemon.json +79 -0
  164. data/lib/seed_data/20230613121411/doto/themes/light.json +79 -0
  165. data/lib/seed_data/20230613121411/doto/themes/matrix.json +79 -0
  166. data/lib/seed_data/20230613121411/doto/themes/whiteout.json +79 -0
  167. data/lib/seed_data/20240210161248/.doto +9 -0
  168. data/lib/seed_data/20240210161248/doto/current_project.json +4 -0
  169. data/lib/seed_data/20240210161248/doto/migration_version.json +3 -0
  170. data/lib/seed_data/20240210161248/doto/projects/default/project.json +5 -0
  171. data/lib/seed_data/20240210161248/doto/themes/cherry.json +79 -0
  172. data/lib/seed_data/20240210161248/doto/themes/christmas.json +79 -0
  173. data/lib/seed_data/20240210161248/doto/themes/default.json +79 -0
  174. data/lib/seed_data/20240210161248/doto/themes/lemon.json +79 -0
  175. data/lib/seed_data/20240210161248/doto/themes/light.json +79 -0
  176. data/lib/seed_data/20240210161248/doto/themes/matrix.json +79 -0
  177. data/lib/seed_data/20240210161248/doto/themes/whiteout.json +79 -0
  178. data/sig/dsu.rbs +4 -0
  179. 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