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.
- checksums.yaml +7 -0
- data/.env.test +1 -0
- data/.reek.yml +20 -0
- data/.rspec +3 -0
- data/.rubocop.yml +206 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +30 -0
- data/Gemfile.lock +179 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/Rakefile +16 -0
- data/bin/console +36 -0
- data/bin/doto +3 -0
- data/bin/setup +18 -0
- data/exe/doto +33 -0
- data/lib/core/ruby/color_theme_colors.rb +16 -0
- data/lib/core/ruby/color_theme_mode.rb +42 -0
- data/lib/core/ruby/wrap_and_join.rb +31 -0
- data/lib/doto/base_cli.rb +56 -0
- data/lib/doto/cli.rb +131 -0
- data/lib/doto/command_services/add_entry_service.rb +50 -0
- data/lib/doto/crud/json_file.rb +161 -0
- data/lib/doto/env.rb +44 -0
- data/lib/doto/migration/base_service.rb +118 -0
- data/lib/doto/migration/migrator.rb +24 -0
- data/lib/doto/migration/raw_helpers/color_theme_hash.rb +13 -0
- data/lib/doto/migration/raw_helpers/configuration_hash.rb +15 -0
- data/lib/doto/migration/raw_helpers/entry_group_hash.rb +13 -0
- data/lib/doto/migration/raw_json_file.rb +15 -0
- data/lib/doto/migration/raw_json_files.rb +56 -0
- data/lib/doto/migration/v20230613121411/service.rb +94 -0
- data/lib/doto/migration/v20240210161248/service.rb +148 -0
- data/lib/doto/migration/version.rb +7 -0
- data/lib/doto/models/color_theme.rb +224 -0
- data/lib/doto/models/configuration.rb +185 -0
- data/lib/doto/models/entry.rb +63 -0
- data/lib/doto/models/entry_group.rb +223 -0
- data/lib/doto/models/migration_version.rb +49 -0
- data/lib/doto/models/project.rb +295 -0
- data/lib/doto/presenters/base_presenter.rb +32 -0
- data/lib/doto/presenters/base_presenter_ex.rb +15 -0
- data/lib/doto/presenters/color_theme_presenter.rb +52 -0
- data/lib/doto/presenters/color_theme_show_presenter.rb +55 -0
- data/lib/doto/presenters/configuration_presenter.rb +50 -0
- data/lib/doto/presenters/entry_group/list/date_presenter.rb +77 -0
- data/lib/doto/presenters/entry_group/list/dates_presenter.rb +60 -0
- data/lib/doto/presenters/entry_group/list/messages.rb +15 -0
- data/lib/doto/presenters/entry_group/list/nothing_to_list.rb +15 -0
- data/lib/doto/presenters/entry_group_presenter.rb +35 -0
- data/lib/doto/presenters/entry_presenter.rb +25 -0
- data/lib/doto/presenters/export/all_presenter.rb +44 -0
- data/lib/doto/presenters/export/dates_presenter.rb +55 -0
- data/lib/doto/presenters/import/all_presenter.rb +57 -0
- data/lib/doto/presenters/import/dates_presenter.rb +70 -0
- data/lib/doto/presenters/import/import_entry.rb +22 -0
- data/lib/doto/presenters/import/import_file.rb +33 -0
- data/lib/doto/presenters/project/create_presenter.rb +44 -0
- data/lib/doto/presenters/project/defaultable.rb +15 -0
- data/lib/doto/presenters/project/delete_by_number_presenter.rb +54 -0
- data/lib/doto/presenters/project/delete_presenter.rb +53 -0
- data/lib/doto/presenters/project/list_presenter.rb +24 -0
- data/lib/doto/presenters/project/rename_by_number_presenter.rb +63 -0
- data/lib/doto/presenters/project/rename_presenter.rb +57 -0
- data/lib/doto/presenters/project/use_by_number_presenter.rb +57 -0
- data/lib/doto/presenters/project/use_presenter.rb +56 -0
- data/lib/doto/services/color_theme/hydrator_service.rb +42 -0
- data/lib/doto/services/configuration/hydrator_service.rb +42 -0
- data/lib/doto/services/entry/hydrator_service.rb +33 -0
- data/lib/doto/services/entry_group/browse_service.rb +100 -0
- data/lib/doto/services/entry_group/counter_service.rb +32 -0
- data/lib/doto/services/entry_group/deleter_service.rb +35 -0
- data/lib/doto/services/entry_group/editor_service.rb +103 -0
- data/lib/doto/services/entry_group/exporter_service.rb +98 -0
- data/lib/doto/services/entry_group/hydrator_service.rb +37 -0
- data/lib/doto/services/entry_group/importer_service.rb +117 -0
- data/lib/doto/services/migration_version/hydrator_service.rb +36 -0
- data/lib/doto/services/project/hydrator_service.rb +40 -0
- data/lib/doto/services/project/rename_service.rb +70 -0
- data/lib/doto/services/stderr_redirector_service.rb +27 -0
- data/lib/doto/services/stdout_redirector_service.rb +27 -0
- data/lib/doto/services/temp_file/reader_service.rb +33 -0
- data/lib/doto/services/temp_file/writer_service.rb +35 -0
- data/lib/doto/subcommands/base_subcommand.rb +12 -0
- data/lib/doto/subcommands/browse.rb +49 -0
- data/lib/doto/subcommands/config.rb +81 -0
- data/lib/doto/subcommands/delete.rb +108 -0
- data/lib/doto/subcommands/edit.rb +48 -0
- data/lib/doto/subcommands/export.rb +62 -0
- data/lib/doto/subcommands/import.rb +72 -0
- data/lib/doto/subcommands/list.rb +95 -0
- data/lib/doto/subcommands/project.rb +146 -0
- data/lib/doto/subcommands/theme.rb +131 -0
- data/lib/doto/support/ask.rb +44 -0
- data/lib/doto/support/color_themable.rb +36 -0
- data/lib/doto/support/command_help_colorizeable.rb +34 -0
- data/lib/doto/support/command_hookable.rb +71 -0
- data/lib/doto/support/command_options/doto_times.rb +48 -0
- data/lib/doto/support/command_options/time.rb +84 -0
- data/lib/doto/support/command_options/time_mnemonic.rb +108 -0
- data/lib/doto/support/command_options/time_mnemonics.rb +16 -0
- data/lib/doto/support/descriptable.rb +29 -0
- data/lib/doto/support/entry_group_browsable.rb +104 -0
- data/lib/doto/support/field_errors.rb +11 -0
- data/lib/doto/support/fileable.rb +136 -0
- data/lib/doto/support/presentable.rb +11 -0
- data/lib/doto/support/project_file_system.rb +118 -0
- data/lib/doto/support/short_string.rb +24 -0
- data/lib/doto/support/time_comparable.rb +21 -0
- data/lib/doto/support/time_formatable.rb +65 -0
- data/lib/doto/support/times_sortable.rb +71 -0
- data/lib/doto/support/transform_project_name.rb +24 -0
- data/lib/doto/support/utils.rb +11 -0
- data/lib/doto/validators/color_theme_validator.rb +74 -0
- data/lib/doto/validators/description_validator.rb +51 -0
- data/lib/doto/validators/entries_validator.rb +77 -0
- data/lib/doto/validators/project_name_validator.rb +58 -0
- data/lib/doto/validators/time_validator.rb +25 -0
- data/lib/doto/validators/version_validator.rb +29 -0
- data/lib/doto/version.rb +6 -0
- data/lib/doto/views/base_list_view.rb +41 -0
- data/lib/doto/views/color_theme/index.rb +62 -0
- data/lib/doto/views/color_theme/show.rb +107 -0
- data/lib/doto/views/configuration/show.rb +41 -0
- data/lib/doto/views/entry_group/edit.rb +121 -0
- data/lib/doto/views/entry_group/list.rb +23 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display.rb +53 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_month_of.rb +32 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_week_of.rb +33 -0
- data/lib/doto/views/entry_group/shared/no_entries_to_display_for_year_of.rb +33 -0
- data/lib/doto/views/entry_group/show.rb +63 -0
- data/lib/doto/views/export.rb +82 -0
- data/lib/doto/views/import.rb +105 -0
- data/lib/doto/views/import_dates.rb +17 -0
- data/lib/doto/views/project/create.rb +87 -0
- data/lib/doto/views/project/delete.rb +96 -0
- data/lib/doto/views/project/delete_by_number.rb +19 -0
- data/lib/doto/views/project/list.rb +115 -0
- data/lib/doto/views/project/rename.rb +98 -0
- data/lib/doto/views/project/rename_by_number.rb +21 -0
- data/lib/doto/views/project/use.rb +97 -0
- data/lib/doto/views/project/use_by_number.rb +19 -0
- data/lib/doto/views/shared/error.rb +17 -0
- data/lib/doto/views/shared/info.rb +17 -0
- data/lib/doto/views/shared/message.rb +85 -0
- data/lib/doto/views/shared/model_errors.rb +32 -0
- data/lib/doto/views/shared/success.rb +17 -0
- data/lib/doto/views/shared/warning.rb +17 -0
- data/lib/doto.rb +33 -0
- data/lib/locales/en/active_record.yml +17 -0
- data/lib/locales/en/commands.yml +165 -0
- data/lib/locales/en/miscellaneous.yml +29 -0
- data/lib/locales/en/presenters.yml +19 -0
- data/lib/locales/en/services.yml +14 -0
- data/lib/locales/en/subcommands.yml +786 -0
- data/lib/seed_data/0/.todo +5 -0
- data/lib/seed_data/20230613121411/.doto +8 -0
- data/lib/seed_data/20230613121411/doto/migration_version.json +3 -0
- data/lib/seed_data/20230613121411/doto/themes/cherry.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/christmas.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/default.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/lemon.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/light.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/matrix.json +79 -0
- data/lib/seed_data/20230613121411/doto/themes/whiteout.json +79 -0
- data/lib/seed_data/20240210161248/.doto +9 -0
- data/lib/seed_data/20240210161248/doto/current_project.json +4 -0
- data/lib/seed_data/20240210161248/doto/migration_version.json +3 -0
- data/lib/seed_data/20240210161248/doto/projects/default/project.json +5 -0
- data/lib/seed_data/20240210161248/doto/themes/cherry.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/christmas.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/default.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/lemon.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/light.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/matrix.json +79 -0
- data/lib/seed_data/20240210161248/doto/themes/whiteout.json +79 -0
- data/sig/dsu.rbs +4 -0
- 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
|