doto 0.0.1.pre.alpha.1

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