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,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require_relative '../crud/json_file'
5
+ require_relative '../migration/version'
6
+ require_relative '../support/fileable'
7
+ require_relative '../support/presentable'
8
+ require_relative '../validators/version_validator'
9
+
10
+ module Doto
11
+ module Models
12
+ # This class represents the doto configuration.
13
+ class Configuration < Crud::JsonFile
14
+ include Support::Fileable
15
+ include Support::Presentable
16
+
17
+ VERSION = Migration::VERSION
18
+
19
+ DEFAULT_CONFIGURATION = {
20
+ version: VERSION,
21
+ # The default editor to use when editing entry groups if the EDITOR
22
+ # environment variable on your system is not set. On nix systmes,
23
+ # the default editor is`nano`. You need to change this default on
24
+ # Windows systems.
25
+ editor: 'nano',
26
+ # The order by which entries should be displayed by default:
27
+ # :asc or :desc, ascending or descending, respectively.
28
+ entries_display_order: :desc,
29
+ carry_over_entries_to_today: false,
30
+ # If true, when using doto commands that list date ranges (e.g.
31
+ # `doto list dates`), the displayed list will include dates that
32
+ # have no doto entries. If false, the displayed list will only
33
+ # include dates that have doto entries.
34
+ # For all other `doto list` commands, if true, this option will
35
+ # behave in the aforementioned manner. If false, the displayed
36
+ # list will unconditionally display the first and last dates
37
+ # regardless of whether or not the TODO date has entries or not;
38
+ # all other dates will not be displayed if the TODO date has no
39
+ # entries.
40
+ include_all: false,
41
+ # Themes
42
+ # The currently selected color theme. Should be equal to
43
+ # Models::ColorTheme::DEFAULT_THEME_NAME or the name of a custom
44
+ # theme (with the same file name) that resides in the themes_folder.
45
+ theme_name: 'default',
46
+ # The default project to use.
47
+ default_project: 'default'
48
+ }.freeze
49
+
50
+ validates_with Validators::VersionValidator
51
+ validates :editor, presence: true
52
+ validates :entries_display_order, presence: true,
53
+ inclusion: { in: %i[asc desc], message: 'must be :asc or :desc' }
54
+ validates :carry_over_entries_to_today, inclusion: { in: [true, false], message: 'must be true or false' }
55
+ validates :include_all, inclusion: { in: [true, false], message: 'must be true or false' }
56
+ validates :theme_name, presence: true
57
+ validate :validate_theme_file
58
+ validates :default_project, presence: true
59
+ validate :validate_default_project
60
+
61
+ attr_accessor :version,
62
+ :editor,
63
+ :entries_display_order,
64
+ :carry_over_entries_to_today,
65
+ :include_all,
66
+ :theme_name,
67
+ :default_project
68
+
69
+ attr_reader :options
70
+
71
+ alias exist? file_exist?
72
+
73
+ def initialize(options: {})
74
+ super(config_path)
75
+
76
+ FileUtils.mkdir_p config_folder
77
+
78
+ @options = options || {}
79
+ reload
80
+
81
+ write! unless exist?
82
+ end
83
+
84
+ class << self
85
+ def exist?
86
+ File.exist?(Support::Fileable.config_path)
87
+ end
88
+ end
89
+
90
+ # Temporarily sets the configuration to the given config_hash.
91
+ # To reset the configuration to its original state, call #reload
92
+ def replace!(config_hash: {})
93
+ raise ArgumentError, 'config_hash is nil.' if config_hash.nil?
94
+ raise ArgumentError, "config_hash must be a Hash: \"#{config_hash}\"." unless config_hash.is_a?(Hash)
95
+
96
+ assign_attributes_from config_hash.dup
97
+
98
+ self
99
+ end
100
+
101
+ # Restores the configuration to its original state from disk.
102
+ def reload
103
+ file_hash = if exist?
104
+ read do |config_hash|
105
+ hydrated_hash = Services::Configuration::HydratorService.new(config_hash: config_hash).call
106
+ config_hash.merge!(hydrated_hash)
107
+ end
108
+ else
109
+ DEFAULT_CONFIGURATION.dup
110
+ end
111
+
112
+ assign_attributes_from file_hash
113
+
114
+ self
115
+ end
116
+
117
+ def carry_over_entries_to_today?
118
+ carry_over_entries_to_today
119
+ end
120
+
121
+ def to_h
122
+ {
123
+ version: version,
124
+ editor: editor,
125
+ entries_display_order: entries_display_order,
126
+ carry_over_entries_to_today: carry_over_entries_to_today,
127
+ include_all: include_all,
128
+ theme_name: theme_name,
129
+ default_project: default_project
130
+ }
131
+ end
132
+
133
+ # Override == and hash so that we can compare objects based
134
+ # on attributes alone. This is also useful for comparing objects
135
+ # in an array, for example.
136
+ def ==(other)
137
+ return false unless other.is_a?(Configuration)
138
+
139
+ to_h == other.to_h
140
+ end
141
+ alias eql? ==
142
+
143
+ def hash
144
+ DEFAULT_CONFIGURATION.each_key.map do |key|
145
+ public_send(key)
146
+ end.hash
147
+ end
148
+
149
+ def merge(hash)
150
+ hash.transform_keys!(&:to_sym)
151
+ replace!(config_hash: to_h.merge(hash))
152
+ end
153
+
154
+ private
155
+
156
+ def assign_attributes_from(config_hash)
157
+ @version = config_hash.fetch(:version, VERSION)
158
+ @editor = config_hash.fetch(:editor, DEFAULT_CONFIGURATION[:editor])
159
+ @entries_display_order = config_hash.fetch(:entries_display_order,
160
+ DEFAULT_CONFIGURATION[:entries_display_order])
161
+ @carry_over_entries_to_today = config_hash.fetch(:carry_over_entries_to_today,
162
+ DEFAULT_CONFIGURATION[:carry_over_entries_to_today])
163
+ @include_all = config_hash.fetch(:include_all, DEFAULT_CONFIGURATION[:include_all])
164
+ @theme_name = config_hash.fetch(:theme_name, DEFAULT_CONFIGURATION[:theme_name])
165
+ @default_project = config_hash.fetch(:default_project, DEFAULT_CONFIGURATION[:default_project])
166
+ end
167
+
168
+ def validate_theme_file
169
+ theme_path = themes_path(theme_name: theme_name)
170
+ return if File.exist?(theme_path)
171
+
172
+ i18n_key = 'configuration.errors.theme_file_missing'
173
+ errors.add(:base, I18n.t(i18n_key, theme_path: theme_path))
174
+ end
175
+
176
+ def validate_default_project
177
+ default_project_folder = File.join(projects_folder, default_project.presence || '{{blank}}')
178
+ return if Dir.exist?(default_project_folder)
179
+
180
+ i18n_key = 'configuration.errors.project_path_missing'
181
+ errors.add(:base, I18n.t(i18n_key, project_folder: default_project_folder))
182
+ end
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_model'
4
+ require_relative '../support/descriptable'
5
+ require_relative '../support/presentable'
6
+ require_relative '../validators/description_validator'
7
+
8
+ module Doto
9
+ module Models
10
+ # This class represents something someone might want to share at their
11
+ # daily standup (TODO).
12
+ class Entry
13
+ include ActiveModel::Model
14
+ include Support::Descriptable
15
+ include Support::Presentable
16
+
17
+ MIN_DESCRIPTION_LENGTH = 2
18
+ MAX_DESCRIPTION_LENGTH = 256
19
+
20
+ validates_with Validators::DescriptionValidator
21
+
22
+ attr_reader :description, :options
23
+
24
+ def initialize(description:, options: {})
25
+ raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
26
+
27
+ # Make sure to call the setter method so that the description is cleaned up.
28
+ self.description = description
29
+ @options = options || {}
30
+ end
31
+
32
+ class << self
33
+ def clean_description(description)
34
+ return if description.nil?
35
+
36
+ description.strip.gsub(/\s+/, ' ')
37
+ end
38
+ end
39
+
40
+ def description=(description)
41
+ @description = self.class.clean_description description
42
+ end
43
+
44
+ def to_h
45
+ { description: description }
46
+ end
47
+
48
+ # Override == and hash so that we can compare Entry objects based
49
+ # on description alone. This is useful for comparing entries in
50
+ # an array, for example.
51
+ def ==(other)
52
+ return false unless other.is_a?(Entry)
53
+
54
+ description == other.description
55
+ end
56
+ alias eql? ==
57
+
58
+ def hash
59
+ description.hash
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,223 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../crud/json_file'
4
+ require_relative '../migration/version'
5
+ require_relative '../services/entry_group/editor_service'
6
+ require_relative '../support/fileable'
7
+ require_relative '../support/presentable'
8
+ require_relative '../support/time_comparable'
9
+ require_relative '../support/time_formatable'
10
+ require_relative '../validators/entries_validator'
11
+ require_relative '../validators/time_validator'
12
+ require_relative '../validators/version_validator'
13
+ require_relative 'entry'
14
+
15
+ module Doto
16
+ module Models
17
+ # This class represents a group of entries for a given day. IOW,
18
+ # things someone might want to share at their daily standup (TODO).
19
+ class EntryGroup < Crud::JsonFile
20
+ include Support::Fileable
21
+ include Support::Presentable
22
+ include Support::TimeComparable
23
+ include Support::TimeFormatable
24
+
25
+ ENTRIES_FILE_NAME_REGEX = /\d{4}-\d{2}-\d{2}.json/
26
+ ENTRIES_FILE_NAME_TIME_REGEX = /\d{4}-\d{2}-\d{2}/
27
+ VERSION = Migration::VERSION
28
+
29
+ attr_accessor :time, :version
30
+ attr_reader :entries, :options
31
+
32
+ validates_with Validators::EntriesValidator
33
+ validates_with Validators::TimeValidator
34
+ validates_with Validators::VersionValidator
35
+
36
+ def initialize(time: nil, entries: nil, version: nil, options: {})
37
+ raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
38
+ raise ArgumentError, 'version is the wrong object type' unless version.is_a?(Integer) || version.nil?
39
+
40
+ FileUtils.mkdir_p(entries_folder)
41
+
42
+ @time = ensure_local_time(time)
43
+
44
+ super(entries_path(time: @time))
45
+
46
+ @version = version || VERSION
47
+ self.entries = entries || []
48
+ @options = options || {}
49
+ end
50
+
51
+ # Override == and hash so that we can compare Entry Group objects.
52
+ def ==(other)
53
+ return false unless other.is_a?(EntryGroup) &&
54
+ version == other.version &&
55
+ time_equal?(other_time: other.time)
56
+
57
+ entries == other.entries
58
+ end
59
+ alias eql? ==
60
+
61
+ def clone
62
+ self.class.new(time: time, entries: entries.map(&:clone), version: version)
63
+ end
64
+
65
+ def delete
66
+ self.class.delete(time: time)
67
+ entries.clear
68
+ end
69
+
70
+ def delete!
71
+ self.class.delete!(time: time)
72
+ entries.clear
73
+ end
74
+
75
+ def entries=(entries)
76
+ entries ||= []
77
+
78
+ raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array)
79
+ raise ArgumentError, 'entries contains the wrong object type' unless entries.all?(Entry)
80
+
81
+ @entries = entries.map(&:clone)
82
+ end
83
+
84
+ def exist?
85
+ self.class.exist?(time: time)
86
+ end
87
+
88
+ def hash
89
+ entries.map(&:hash).tap do |hashes|
90
+ hashes << version.hash
91
+ hashes << time_equal_compare_string_for(time: time)
92
+ end.hash
93
+ end
94
+
95
+ def time_formatted
96
+ formatted_time(time: time)
97
+ end
98
+
99
+ def time_yyyy_mm_dd
100
+ yyyy_mm_dd(time: time)
101
+ end
102
+
103
+ def to_h
104
+ {
105
+ version: version,
106
+ time: time.dup,
107
+ entries: entries.map(&:to_h)
108
+ }
109
+ end
110
+
111
+ def valid_unique_entries
112
+ entries&.select(&:valid?)&.uniq(&:description)
113
+ end
114
+
115
+ class << self
116
+ def all
117
+ entry_files.filter_map do |file_path|
118
+ entry_file_name = File.basename(file_path)
119
+ next unless entry_file_name.match?(ENTRIES_FILE_NAME_REGEX)
120
+
121
+ entry_date = File.basename(entry_file_name, '.*')
122
+ find time: Time.parse(entry_date)
123
+ end
124
+ end
125
+
126
+ def any?
127
+ entry_files.any? do |file_path|
128
+ entry_date = File.basename(file_path, '.*')
129
+ entry_date.match?(ENTRIES_FILE_NAME_TIME_REGEX)
130
+ end
131
+ end
132
+
133
+ def delete(time:)
134
+ superclass.delete(file_path: entries_path_for(time: time))
135
+ end
136
+
137
+ def delete!(time:)
138
+ superclass.delete!(file_path: entries_path_for(time: time))
139
+ end
140
+
141
+ def edit(time:, options: {})
142
+ # NOTE: Uncomment this line to prohibit edits on
143
+ # Entry Groups that do not exist (i.e. have no entries).
144
+ # return new(time: time) unless exists?(time: time)
145
+
146
+ find_or_initialize(time: time).tap do |entry_group|
147
+ Services::EntryGroup::EditorService.new(entry_group: entry_group, options: options).call
148
+ end
149
+ end
150
+
151
+ def exist?(time:)
152
+ superclass.file_exist?(file_path: entries_path_for(time: time))
153
+ end
154
+
155
+ def entry_group_times(between: nil)
156
+ entry_files.filter_map do |file_path|
157
+ entry_file_name = File.basename(file_path)
158
+ next unless entry_file_name.match?(ENTRIES_FILE_NAME_REGEX)
159
+
160
+ time = File.basename(entry_file_name, '.*')
161
+ next if between && !Time.parse(time).between?(between.min, between.max)
162
+
163
+ time
164
+ end
165
+ end
166
+
167
+ def entry_groups(between:)
168
+ entry_group_times(between: between).filter_map do |time|
169
+ Models::EntryGroup.find(time: Time.parse(time))
170
+ end
171
+ end
172
+
173
+ def find(time:)
174
+ file_path = entries_path_for(time: time)
175
+ entry_group_hash = read!(file_path: file_path)
176
+ Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
177
+ end
178
+
179
+ def find_or_initialize(time:)
180
+ file_path = entries_path_for(time: time)
181
+ read(file_path: file_path) do |entry_group_hash|
182
+ Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
183
+ end || new(time: time)
184
+ end
185
+
186
+ def write(file_data:, file_path:)
187
+ if file_data[:entries].empty?
188
+ superclass.delete(file_path: file_path)
189
+ return true
190
+ end
191
+
192
+ super
193
+ end
194
+
195
+ def write!(file_data:, file_path:)
196
+ if file_data[:entries].empty?
197
+ superclass.delete!(file_path: file_path)
198
+ return
199
+ end
200
+
201
+ super
202
+ end
203
+
204
+ private
205
+
206
+ def entries_path_for(time:)
207
+ Support::Fileable.entries_path(time: time)
208
+ end
209
+
210
+ def entry_files
211
+ Dir.glob("#{Support::Fileable.entries_folder}/*")
212
+ end
213
+ end
214
+
215
+ private
216
+
217
+ def ensure_local_time(time)
218
+ time ||= Time.now
219
+ time.in_time_zone
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../crud/json_file'
4
+ require_relative '../services/migration_version/hydrator_service'
5
+ require_relative '../validators/version_validator'
6
+
7
+ module Doto
8
+ module Models
9
+ # This class represents a doto migration_version.
10
+ class MigrationVersion < Crud::JsonFile
11
+ include Support::Fileable
12
+
13
+ attr_reader :options
14
+
15
+ alias exist? file_exist?
16
+
17
+ def initialize(version: nil, options: {})
18
+ super(migration_version_path)
19
+
20
+ FileUtils.mkdir_p migration_version_folder
21
+
22
+ @options = options || {}
23
+ @version = version and return if version
24
+
25
+ file_hash = if exist?
26
+ read do |migration_version_hash|
27
+ hydrated_hash =
28
+ Services::MigrationVersion::HydratorService.new(migration_version_hash: migration_version_hash).call
29
+ migration_version_hash.merge!(hydrated_hash)
30
+ end
31
+ end
32
+
33
+ self.version = file_hash.try(:[], :version) || 0
34
+ end
35
+
36
+ # Returns true if the current doto install is the
37
+ # current migration version.
38
+ def current_migration?
39
+ version == Doto::Migration::VERSION
40
+ end
41
+
42
+ def to_h
43
+ {
44
+ version: version
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end