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
data/exe/doto ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'rubygems'
5
+
6
+ lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ $LOAD_PATH << lib_dir unless $LOAD_PATH.include?(lib_dir)
8
+
9
+ if File.exist?('.env.development')
10
+ # This loads our development environment when running dev.
11
+ require 'dotenv'
12
+ Dotenv.load('.env.development')
13
+ end
14
+
15
+ require 'doto'
16
+
17
+ if Doto.env.development?
18
+ # This simply allows us to use a folder other than doto folder for the production
19
+ # release, so we don't mess with our current production doto folder.
20
+ module Doto
21
+ module Support
22
+ module Fileable
23
+ def root_folder
24
+ File.join(gem_dir, '.development_home')
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ puts "ATTENTION: Doto root folder is: #{Doto::Support::Fileable.root_folder}!"
31
+ end
32
+
33
+ Doto::CLI.start(ARGV)
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ColorThemeColors
4
+ DEFAULT_THEME_COLORS = { color: :default, mode: :default, background: :default }.freeze
5
+
6
+ # Ensures that default colors and mode are represented in the returned Hash.
7
+ def merge_default_colors
8
+ # TODO: Error checking.
9
+ DEFAULT_THEME_COLORS.merge(dup)
10
+ end
11
+
12
+ def merge_default_colors!
13
+ # TODO: Error checking.
14
+ merge!(merge_default_colors)
15
+ end
16
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TODO: Error checking.
4
+ # TODO: Keep <method>! (bang)?
5
+ module ColorThemeMode
6
+ def default!
7
+ dup.merge({ mode: :default })
8
+ end
9
+
10
+ def bold!
11
+ dup.merge({ mode: :bold })
12
+ end
13
+
14
+ def italic!
15
+ dup.merge({ mode: :italic })
16
+ end
17
+
18
+ def underline!
19
+ dup.merge({ mode: :underline })
20
+ end
21
+
22
+ def blink!
23
+ dup.merge({ mode: :blink })
24
+ end
25
+
26
+ def swap!
27
+ dup.merge({ mode: :swap })
28
+ end
29
+
30
+ def hide!
31
+ dup.merge({ mode: :hide })
32
+ end
33
+
34
+ def mode!(mode)
35
+ dup.merge({ mode: mode })
36
+ end
37
+
38
+ def light!
39
+ light_color = :"light_#{self[:color].to_s.gsub('light_', '')}"
40
+ dup.merge({ color: light_color })
41
+ end
42
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module WrapAndJoin
4
+ class << self
5
+ def included(base)
6
+ base.const_set(:WRAP_AND_JOIN_JOIN_TOKEN, ', ')
7
+ end
8
+ end
9
+
10
+ def wrap_and_join(wrapper: %w["], join: Array::WRAP_AND_JOIN_JOIN_TOKEN)
11
+ validate_wrapper!(wrapper)
12
+ validate_join!(join)
13
+
14
+ wrapper << wrapper.first if wrapper.count == 1
15
+ map { |element| "#{wrapper[0]}#{element}#{wrapper[1]}" }.join(join)
16
+ end
17
+
18
+ private
19
+
20
+ def validate_wrapper!(wrapper)
21
+ raise ArgumentError, 'wrapper is nil' if wrapper.nil?
22
+ raise ArgumentError, 'wrapper must be an Array' unless wrapper.is_a?(Array)
23
+ raise ArgumentError, 'wrapper must be an Array of 1 or 2 wrapper elements' unless wrapper.count.between?(1, 2)
24
+ end
25
+
26
+ def validate_join!(join)
27
+ return if join.nil?
28
+
29
+ raise ArgumentError, 'join must be a String' unless join.is_a?(String)
30
+ end
31
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ require 'thor'
5
+ require_relative 'command_services/add_entry_service'
6
+ require_relative 'models/color_theme'
7
+ require_relative 'models/configuration'
8
+ require_relative 'models/entry_group'
9
+ require_relative 'services/stdout_redirector_service'
10
+ require_relative 'support/color_themable'
11
+ require_relative 'support/command_help_colorizeable'
12
+ require_relative 'support/command_hookable'
13
+ require_relative 'support/times_sortable'
14
+ require_relative 'version'
15
+ require_relative 'views/entry_group/show'
16
+
17
+ module Doto
18
+ class BaseCLI < ::Thor
19
+ include Support::ColorThemable
20
+ include Support::CommandHelpColorizable
21
+ include Support::CommandHookable
22
+ include Support::TimesSortable
23
+
24
+ class_option :debug, type: :boolean, default: false
25
+
26
+ default_command :help
27
+
28
+ def initialize(*args)
29
+ super
30
+
31
+ @configuration = Models::Configuration.new
32
+ end
33
+
34
+ class << self
35
+ def exit_on_failure?
36
+ false
37
+ end
38
+
39
+ def date_option_description
40
+ I18n.t('options.date_option_description')
41
+ end
42
+
43
+ def mnemonic_option_description
44
+ I18n.t('options.mnemonic_option_description')
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :configuration
51
+
52
+ def color_theme
53
+ Models::ColorTheme.current_or_default
54
+ end
55
+ end
56
+ end
data/lib/doto/cli.rb ADDED
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'time'
5
+ require_relative 'base_cli'
6
+ require_relative 'presenters/entry_group/list/date_presenter'
7
+ require_relative 'subcommands/browse'
8
+ require_relative 'subcommands/config'
9
+ require_relative 'subcommands/delete'
10
+ require_relative 'subcommands/edit'
11
+ require_relative 'subcommands/export'
12
+ require_relative 'subcommands/import'
13
+ require_relative 'subcommands/list'
14
+ require_relative 'subcommands/project'
15
+ require_relative 'subcommands/theme'
16
+ require_relative 'support/command_options/time_mnemonic'
17
+ require_relative 'support/time_formatable'
18
+ require_relative 'views/entry_group/list'
19
+
20
+ module Doto
21
+ # The `doto` command.
22
+ class CLI < BaseCLI
23
+ include Support::CommandOptions::TimeMnemonic
24
+ include Support::TimeFormatable
25
+
26
+ map I18n.t('commands.add.key_mappings') => :add
27
+ map I18n.t('commands.browse.key_mappings') => :browse
28
+ map I18n.t('commands.config.key_mappings') => :config
29
+ map I18n.t('commands.delete.key_mappings') => :delete
30
+ map I18n.t('commands.edit.key_mappings') => :edit
31
+ map I18n.t('commands.export.key_mappings') => :export
32
+ map I18n.t('commands.help.key_mappings') => :help
33
+ map I18n.t('commands.import.key_mappings') => :import
34
+ map I18n.t('commands.info.key_mappings') => :info
35
+ map I18n.t('commands.list.key_mappings') => :list
36
+ map I18n.t('commands.project.key_mappings') => :project
37
+ map I18n.t('commands.theme.key_mappings') => :theme
38
+ map I18n.t('commands.version.key_mappings') => :version
39
+
40
+ desc I18n.t('commands.add.desc'), I18n.t('commands.add.usage')
41
+ long_desc I18n.t('commands.add.long_desc',
42
+ date_option_description: date_option_description, mnemonic_option_description: mnemonic_option_description)
43
+ option I18n.t('options.date_or_mnemonic.name'), aliases: I18n.t('options.date_or_mnemonic.aliases'),
44
+ type: :string, banner: I18n.t('options.date_or_mnemonic.banner')
45
+ option I18n.t('options.tomorrow.name'), aliases: I18n.t('options.tomorrow.aliases'), type: :boolean
46
+ option I18n.t('options.yesterday.name'), aliases: I18n.t('options.yesterday.aliases'), type: :boolean
47
+ option I18n.t('options.today.name'), aliases: I18n.t('options.today.aliases'), type: :boolean, default: true
48
+ def add(description)
49
+ date_or_mnemonic = if options[I18n.t('options.date.name')].present?
50
+ options[I18n.t('options.date.name')]
51
+ elsif options[I18n.t('options.tomorrow.name')].present?
52
+ I18n.t('options.tomorrow.name')
53
+ elsif options[I18n.t('options.yesterday.name')].present?
54
+ I18n.t('options.yesterday.name')
55
+ elsif options[I18n.t('options.today.name')].present?
56
+ I18n.t('options.today.name')
57
+ end
58
+ time = if time_mnemonic?(date_or_mnemonic)
59
+ time_from_mnemonic(command_option: date_or_mnemonic)
60
+ else
61
+ Time.parse(date_or_mnemonic)
62
+ end
63
+ entry = Models::Entry.new(description: description)
64
+ CommandServices::AddEntryService.new(entry: entry, time: time).call
65
+ presenter = Presenters::EntryGroup::List::DatePresenter.new(times: [time], options: options)
66
+ # TODO: Refactor View::EntryGroup::Show to accept a presenter and use it here
67
+ Views::EntryGroup::List.new(presenter: presenter).render
68
+ rescue ArgumentError => e
69
+ Views::Shared::Error.new(messages: e.message).render
70
+ end
71
+
72
+ desc I18n.t('commands.browse.desc'), I18n.t('commands.browse.usage')
73
+ subcommand :browse, Subcommands::Browse
74
+
75
+ desc I18n.t('commands.list.desc'), I18n.t('commands.list.usage')
76
+ subcommand :list, Subcommands::List
77
+
78
+ desc I18n.t('commands.project.desc'), I18n.t('commands.project.usage')
79
+ subcommand :project, Subcommands::Project
80
+
81
+ desc I18n.t('commands.config.desc'), I18n.t('commands.config.usage')
82
+ subcommand :config, Subcommands::Config
83
+
84
+ desc I18n.t('commands.delete.desc'), I18n.t('commands.delete.usage')
85
+ subcommand :delete, Subcommands::Delete
86
+
87
+ desc I18n.t('commands.edit.desc'), I18n.t('commands.edit.usage')
88
+ subcommand :edit, Subcommands::Edit
89
+
90
+ desc I18n.t('commands.export.desc'), I18n.t('commands.export.usage')
91
+ subcommand :export, Subcommands::Export
92
+
93
+ desc I18n.t('commands.theme.desc'), I18n.t('commands.theme.usage')
94
+ subcommand :theme, Subcommands::Theme
95
+
96
+ desc I18n.t('commands.import.desc'), I18n.t('commands.import.usage')
97
+ subcommand :import, Subcommands::Import
98
+
99
+ desc I18n.t('commands.info.desc'), I18n.t('commands.info.usage')
100
+ def info
101
+ configuration_version = Models::Configuration::VERSION
102
+ entry_group_version = Models::EntryGroup::VERSION
103
+ color_theme_version = Models::ColorTheme::VERSION
104
+ info = I18n.t('commands.info.info',
105
+ doto_version: doto_version,
106
+ configuration_version: configuration_version,
107
+ entry_group_version: entry_group_version,
108
+ color_theme_version: color_theme_version,
109
+ config_folder: Support::Fileable.config_path,
110
+ root_folder: Support::Fileable.root_folder,
111
+ entries_folder: Support::Fileable.entries_folder,
112
+ themes_folder: Support::Fileable.themes_folder,
113
+ gem_folder: Support::Fileable.gem_dir,
114
+ temp_folder: Support::Fileable.temp_folder,
115
+ migration_version_folder: Support::Fileable.migration_version_folder,
116
+ migration_file_folder: Support::Fileable.migration_version_path)
117
+ puts apply_theme(info, theme_color: color_theme.body)
118
+ end
119
+
120
+ desc I18n.t('commands.version.desc'), I18n.t('commands.version.usage')
121
+ def version
122
+ puts apply_theme(doto_version, theme_color: color_theme.body)
123
+ end
124
+
125
+ private
126
+
127
+ def doto_version
128
+ "v#{VERSION}"
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/entry'
4
+ require_relative '../support/color_themable'
5
+ require_relative '../support/descriptable'
6
+ require_relative '../support/fileable'
7
+ require_relative '../views/shared/error'
8
+
9
+ module Doto
10
+ module CommandServices
11
+ # This class adds (does NOT update) an entry to an entry group by
12
+ # writing it to the appropriate entry group json file.
13
+ class AddEntryService
14
+ include Support::ColorThemable
15
+ include Support::Descriptable
16
+ include Support::Fileable
17
+
18
+ attr_reader :entry, :entry_group, :time
19
+
20
+ delegate :description, to: :entry
21
+
22
+ # :entry is an Entry object
23
+ # :time is a Time object; the time of the entry group.
24
+ def initialize(entry:, time:)
25
+ raise ArgumentError, 'entry is nil' if entry.nil?
26
+ raise ArgumentError, 'entry is the wrong object type' unless entry.is_a?(Models::Entry)
27
+ raise ArgumentError, 'time is nil' if time.nil?
28
+ raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time)
29
+
30
+ @entry = entry
31
+ @time = time
32
+ @entry_group = Models::EntryGroup.find_or_initialize(time: time)
33
+ end
34
+
35
+ def call
36
+ entry.validate!
37
+ entry_group.entries << entry
38
+ entry_group.save!
39
+ entry
40
+ rescue ActiveModel::ValidationError => e
41
+ header = I18n.t('headers.entry.could_not_be_added')
42
+ Views::Shared::Error.new(messages: e.message, header: header).render
43
+ end
44
+
45
+ private
46
+
47
+ attr_writer :entry, :entry_group, :time
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require 'json'
5
+ require_relative '../migration/version'
6
+
7
+ module Doto
8
+ module Crud
9
+ class JsonFile
10
+ include ActiveModel::Model
11
+
12
+ attr_reader :file_path
13
+
14
+ def initialize(file_path)
15
+ @file_path = file_path
16
+ end
17
+
18
+ def update_version!
19
+ @version = Migration::VERSION
20
+ end
21
+
22
+ def delete
23
+ self.class.delete(file_path: file_path)
24
+ end
25
+
26
+ def delete!
27
+ self.class.delete!(file_path: file_path)
28
+ end
29
+
30
+ def file_exist?
31
+ self.class.file_exist?(file_path: file_path)
32
+ end
33
+
34
+ def persisted?
35
+ file_exist?
36
+ end
37
+
38
+ # Override this method to reload data from the file
39
+ def reload
40
+ @version = read_version
41
+
42
+ self
43
+ end
44
+
45
+ def to_h
46
+ raise NotImplementedError, 'You must implement this method in a your subclass'
47
+ end
48
+
49
+ def to_model
50
+ self
51
+ end
52
+
53
+ def version
54
+ @version ||= read_version
55
+ end
56
+
57
+ def write
58
+ return false unless valid?
59
+
60
+ self.class.write(file_data: to_h, file_path: file_path)
61
+ true
62
+ end
63
+
64
+ def write!
65
+ validate!
66
+
67
+ self.class.write(file_data: to_h, file_path: file_path)
68
+ end
69
+
70
+ alias save write
71
+ alias save! write!
72
+
73
+ class << self
74
+ def file_exist?(file_path:)
75
+ File.exist?(file_path)
76
+ end
77
+
78
+ def delete(file_path:)
79
+ return false unless file_exist?(file_path: file_path)
80
+
81
+ File.delete(file_path)
82
+
83
+ true
84
+ end
85
+
86
+ def delete!(file_path:)
87
+ raise file_does_not_exist_message(file_path: file_path) unless delete(file_path: file_path)
88
+ end
89
+
90
+ def parse(json)
91
+ return if json.nil?
92
+
93
+ JSON.parse(json, symbolize_names: true)
94
+ end
95
+
96
+ def read(file_path:)
97
+ json = File.read(file_path) if file_exist?(file_path: file_path)
98
+ hash = parse(json)
99
+ return yield hash if hash && block_given?
100
+
101
+ hash
102
+ end
103
+
104
+ def read!(file_path:)
105
+ raise file_does_not_exist_message(file_path: file_path) unless file_exist?(file_path: file_path)
106
+
107
+ hash = read(file_path: file_path)
108
+ return yield hash if hash && block_given?
109
+
110
+ hash
111
+ end
112
+
113
+ def write(file_data:, file_path:)
114
+ raise ArgumentError, 'file_data is nil' if file_data.nil?
115
+ raise ArgumentError, "file_data is the wrong object type:\"#{file_data}\"" unless file_data.is_a?(Hash)
116
+
117
+ file_data = JSON.pretty_generate(file_data)
118
+ File.write(file_path, file_data)
119
+ end
120
+
121
+ def write!(file_data:, file_path:)
122
+ # TODO: Should we be raising an error if the file does not exist?
123
+ # raise file_does_not_exist_message(file_path: file_path) unless file_exist?(file_path: file_path)
124
+
125
+ write(file_data: file_data, file_path: file_path)
126
+ end
127
+
128
+ def file_does_not_exist_message(file_path:)
129
+ "File \"#{file_path}\" does not exist"
130
+ end
131
+ end
132
+
133
+ private
134
+
135
+ attr_writer :file_path, :version
136
+
137
+ def read
138
+ hash = self.class.read(file_path: file_path)
139
+ return yield hash if block_given?
140
+
141
+ hash
142
+ end
143
+
144
+ def read!
145
+ hash = self.class.read!(file_path: file_path)
146
+ return yield hash if hash && block_given?
147
+
148
+ hash
149
+ end
150
+
151
+ def read_version
152
+ return 0 unless file_exist?
153
+
154
+ hash = read
155
+ return 0 if hash.nil?
156
+
157
+ hash.fetch(:version, 0).to_i
158
+ end
159
+ end
160
+ end
161
+ end
data/lib/doto/env.rb ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doto
4
+ class << self
5
+ def env # rubocop:disable Metrics/MethodLength
6
+ @env ||= Struct.new(:env) do
7
+ def test?
8
+ env.fetch('DOTO_ENV', nil) == 'test'
9
+ end
10
+
11
+ def development?
12
+ env.fetch('DOTO_ENV', nil) == 'development'
13
+ end
14
+
15
+ def local?
16
+ test? || development?
17
+ end
18
+
19
+ def production?
20
+ env.fetch('DOTO_ENV', 'production') == 'production'
21
+ end
22
+
23
+ def screen_shot_mode?
24
+ development? && (env.fetch('SCREEN_SHOT_USERNAME', '').present? ||
25
+ env.fetch('SCREEN_SHOT_HOSTNAME', '').present?)
26
+ end
27
+
28
+ def screen_shot_prompt
29
+ username = screen_shot_username
30
+ hostname = screen_shot_hostname
31
+ "#{username}@#{hostname}:~ $"
32
+ end
33
+
34
+ def screen_shot_username
35
+ env.fetch('SCREEN_SHOT_USERNAME', 'username')
36
+ end
37
+
38
+ def screen_shot_hostname
39
+ env.fetch('SCREEN_SHOT_HOSTNAME', 'hostname')
40
+ end
41
+ end.new(ENV)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../models/migration_version'
4
+ require_relative '../support/fileable'
5
+ require_relative 'version'
6
+
7
+ module Doto
8
+ module Migration
9
+ class BaseService
10
+ include Support::Fileable
11
+
12
+ def initialize(options: {})
13
+ @options = options || {}
14
+ end
15
+
16
+ class << self
17
+ def migrates_to_latest_migration_version?
18
+ to_migration_version == Migration::VERSION
19
+ end
20
+
21
+ # The migration version that this migration is upgrading from.
22
+ def from_migration_version
23
+ raise NotImplementedError, 'You must implement the #from_migration_version method in your subclass'
24
+ end
25
+
26
+ # The migration version that this migration is upgrading to.
27
+ def to_migration_version
28
+ raise NotImplementedError, 'You must implement the #to_migration_version method in your subclass'
29
+ end
30
+ end
31
+
32
+ def migrate_if!
33
+ return unless run_migration?
34
+
35
+ puts "Running migrations #{from_migration_version} -> #{to_migration_version}..."
36
+ puts "\tpretend?: #{pretend?}" if pretend?
37
+
38
+ run_migration!
39
+ update_migration_version!
40
+
41
+ puts "\tMigration #{from_migration_version} -> #{to_migration_version} complete."
42
+ end
43
+
44
+ private
45
+
46
+ attr_accessor :options
47
+
48
+ # You must implement your own migration logic in your subclass and call this method.
49
+ def run_migration!
50
+ create_backup
51
+ end
52
+
53
+ def pretend?
54
+ options.fetch(:pretend, true)
55
+ end
56
+
57
+ def run_migration?
58
+ migration_version == from_migration_version
59
+ end
60
+
61
+ # The migration version that this migration is upgrading from.
62
+ def from_migration_version
63
+ self.class.from_migration_version
64
+ end
65
+
66
+ # The migration version that this migration is upgrading to.
67
+ def to_migration_version
68
+ self.class.to_migration_version
69
+ end
70
+
71
+ # The migration version before running this migration.
72
+ def migration_version
73
+ # Typically we should not be using models in any of the migration services
74
+ # because if these change, the migrations will break. However, using
75
+ # the MigrationVersion model is an exception because it is a very simple
76
+ # model and is unlikely to change.
77
+ Models::MigrationVersion.new.version
78
+ end
79
+
80
+ def create_backup
81
+ puts "Creating backup #{backup_folder}..."
82
+
83
+ return puts "\tSkipping: backup already exists." if backup_exist?
84
+
85
+ FileUtils.cp_r(doto_folder, backup_folder)
86
+ FileUtils.cp(config_path, backup_folder)
87
+ end
88
+
89
+ def backup_exist?
90
+ Dir.exist?(backup_folder)
91
+ end
92
+
93
+ def backup_folder
94
+ @backup_folder ||= backup_folder_for(migration_version: from_migration_version)
95
+ end
96
+
97
+ def update_migration_version!
98
+ puts 'Updating migration version...'
99
+
100
+ return if pretend? || migration_version == to_migration_version
101
+
102
+ Models::MigrationVersion.new(version: to_migration_version).save!
103
+ end
104
+
105
+ def seed_data_folder
106
+ seed_data_doto_folder_for(migration_version: to_migration_version)
107
+ end
108
+
109
+ def seed_data_configuration
110
+ seed_data_doto_configuration_for(migration_version: to_migration_version)
111
+ end
112
+
113
+ def raise_backup_folder_does_not_exist_error_if!
114
+ raise "Backup folder #{backup_folder} does not exist, cannot continue" unless backup_exist?
115
+ end
116
+ end
117
+ end
118
+ end