doto 0.0.1.pre.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
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