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
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