dsu 1.2.1 → 2.0.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 +4 -4
- data/CHANGELOG.md +65 -21
- data/Gemfile.lock +7 -7
- data/README.md +28 -35
- data/bin/console +23 -1
- data/bin/dsu +3 -0
- data/bin/setup +14 -3
- data/exe/dsu +23 -1
- data/exe/dsu_migrate.rb +43 -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/not_today.rb +7 -0
- data/lib/core/ruby/wrap_and_join.rb +31 -0
- data/lib/dsu/base_cli.rb +19 -23
- data/lib/dsu/cli.rb +47 -37
- data/lib/dsu/command_services/add_entry_service.rb +10 -21
- data/lib/dsu/crud/json_file.rb +139 -0
- data/lib/dsu/crud/raw_json_file.rb +51 -0
- data/lib/dsu/env.rb +21 -0
- data/lib/dsu/migration/service.rb +196 -0
- data/lib/dsu/migration/version.rb +7 -0
- data/lib/dsu/models/color_theme.rb +270 -0
- data/lib/dsu/models/configuration.rb +160 -0
- data/lib/dsu/models/entry.rb +6 -2
- data/lib/dsu/models/entry_group.rb +143 -42
- data/lib/dsu/models/migration_version.rb +48 -0
- data/lib/dsu/presenters/base_presenter.rb +32 -0
- data/lib/dsu/presenters/color_theme_presenter.rb +50 -0
- data/lib/dsu/presenters/color_theme_show_presenter.rb +49 -0
- data/lib/dsu/presenters/configuration_presenter.rb +45 -0
- data/lib/dsu/presenters/entry_group_presenter.rb +35 -0
- data/lib/dsu/presenters/entry_presenter.rb +25 -0
- data/lib/dsu/services/color_theme/hydrator_service.rb +42 -0
- data/lib/dsu/services/configuration/hydrator_service.rb +42 -0
- data/lib/dsu/services/entry/hydrator_service.rb +33 -0
- data/lib/dsu/services/entry_group/editor_service.rb +107 -0
- data/lib/dsu/services/entry_group/hydrator_service.rb +37 -0
- data/lib/dsu/services/migration_version/hydrator_service.rb +36 -0
- data/lib/dsu/services/stderr_redirector_service.rb +27 -0
- data/lib/dsu/services/temp_file/reader_service.rb +33 -0
- data/lib/dsu/services/temp_file/writer_service.rb +35 -0
- data/lib/dsu/subcommands/base_subcommand.rb +14 -0
- data/lib/dsu/subcommands/config.rb +92 -32
- data/lib/dsu/subcommands/edit.rb +3 -3
- data/lib/dsu/subcommands/list.rb +70 -93
- data/lib/dsu/subcommands/theme.rb +159 -0
- data/lib/dsu/support/ask.rb +14 -19
- data/lib/dsu/support/color_themable.rb +34 -0
- data/lib/dsu/support/command_help_colorizeable.rb +27 -0
- data/lib/dsu/support/command_hookable.rb +60 -0
- data/lib/dsu/support/command_options/dsu_times.rb +32 -21
- data/lib/dsu/support/command_options/time.rb +7 -1
- data/lib/dsu/support/command_options/time_mneumonic.rb +7 -1
- data/lib/dsu/support/descriptable.rb +6 -4
- data/lib/dsu/support/entry_group_viewable.rb +28 -4
- data/lib/dsu/support/fileable.rb +94 -0
- data/lib/dsu/support/presentable.rb +11 -0
- data/lib/dsu/support/subcommand_help_colorizeable.rb +27 -0
- data/lib/dsu/support/time_comparable.rb +19 -0
- data/lib/dsu/support/time_formatable.rb +12 -0
- data/lib/dsu/support/times_sortable.rb +48 -14
- data/lib/dsu/support/utils.rb +11 -0
- data/lib/dsu/validators/color_theme_validator.rb +74 -0
- data/lib/dsu/validators/entries_validator.rb +4 -8
- data/lib/dsu/validators/version_validator.rb +29 -0
- data/lib/dsu/version.rb +2 -1
- data/lib/dsu/views/color_theme/index.rb +62 -0
- data/lib/dsu/views/color_theme/show.rb +106 -0
- data/lib/dsu/views/configuration/show.rb +41 -0
- data/lib/dsu/views/entry_group/edit.rb +3 -5
- data/lib/dsu/views/entry_group/shared/no_entries_to_display.rb +41 -0
- data/lib/dsu/views/entry_group/show.rb +16 -15
- data/lib/dsu/views/shared/error.rb +17 -0
- data/lib/dsu/views/shared/info.rb +17 -0
- data/lib/dsu/views/shared/message.rb +85 -0
- data/lib/dsu/views/shared/model_errors.rb +31 -0
- data/lib/dsu/views/shared/success.rb +17 -0
- data/lib/dsu/views/shared/warning.rb +17 -0
- data/lib/dsu.rb +22 -1
- data/lib/seed_data/themes/cherry.json +79 -0
- data/lib/seed_data/themes/default.json +79 -0
- data/lib/seed_data/themes/lemon.json +79 -0
- data/lib/seed_data/themes/matrix.json +79 -0
- data/lib/seed_data/themes/whiteout.json +79 -0
- metadata +68 -23
- data/lib/dsu/core/ruby/not_today.rb +0 -11
- data/lib/dsu/services/ai/tense_translator_service.rb +0 -63
- data/lib/dsu/services/configuration_loader_service.rb +0 -55
- data/lib/dsu/services/entry_group_deleter_service.rb +0 -31
- data/lib/dsu/services/entry_group_editor_service.rb +0 -96
- data/lib/dsu/services/entry_group_hydrator_service.rb +0 -43
- data/lib/dsu/services/entry_group_reader_service.rb +0 -36
- data/lib/dsu/services/entry_group_writer_service.rb +0 -46
- data/lib/dsu/services/entry_hydrator_service.rb +0 -35
- data/lib/dsu/services/temp_file_reader_service.rb +0 -31
- data/lib/dsu/services/temp_file_writer_service.rb +0 -33
- data/lib/dsu/support/colorable.rb +0 -14
- data/lib/dsu/support/configurable.rb +0 -15
- data/lib/dsu/support/configuration.rb +0 -112
- data/lib/dsu/support/entry_group_fileable.rb +0 -49
- data/lib/dsu/support/entry_group_loadable.rb +0 -49
- data/lib/dsu/support/folder_locations.rb +0 -21
- data/lib/dsu/support/say.rb +0 -40
- data/lib/dsu/views/edited_entries/shared/errors.rb +0 -39
- data/lib/dsu/views/shared/messages.rb +0 -56
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require_relative '../crud/json_file'
|
|
5
|
+
require_relative '../migration/version'
|
|
6
|
+
require_relative '../support/color_themable'
|
|
7
|
+
require_relative '../support/descriptable'
|
|
8
|
+
require_relative '../support/fileable'
|
|
9
|
+
require_relative '../support/presentable'
|
|
10
|
+
require_relative '../validators/color_theme_validator'
|
|
11
|
+
require_relative '../validators/description_validator'
|
|
12
|
+
require_relative '../validators/version_validator'
|
|
13
|
+
require_relative 'configuration'
|
|
14
|
+
|
|
15
|
+
module Dsu
|
|
16
|
+
module Models
|
|
17
|
+
# This class represents a dsu color theme.
|
|
18
|
+
class ColorTheme < Crud::JsonFile
|
|
19
|
+
include Support::ColorThemable
|
|
20
|
+
include Support::Descriptable
|
|
21
|
+
include Support::Fileable
|
|
22
|
+
include Support::Presentable
|
|
23
|
+
|
|
24
|
+
VERSION = Migration::VERSION
|
|
25
|
+
|
|
26
|
+
DEFAULT_THEME_NAME = 'default'
|
|
27
|
+
# Theme colors key/value pair format:
|
|
28
|
+
# <key>: { color: <color> [, mode: <mode>] [, background: <background>] }
|
|
29
|
+
# Where <color> (required) == any color represented in the colorize gem `String.colors` array.
|
|
30
|
+
# <mode> (optional, default is :default) == any mode represented in the colorize gem `String.modes` array.
|
|
31
|
+
# <background> (optional, default is :default) == any color represented in the colorize gem
|
|
32
|
+
# `String.colors` array.
|
|
33
|
+
DEFAULT_THEME_COLORS = {
|
|
34
|
+
help: { color: :cyan },
|
|
35
|
+
dsu_header: { color: :white, mode: :bold, background: :cyan },
|
|
36
|
+
dsu_footer: { color: :cyan },
|
|
37
|
+
header: { color: :cyan, mode: :bold },
|
|
38
|
+
subheader: { color: :cyan, mode: :underline },
|
|
39
|
+
body: { color: :cyan },
|
|
40
|
+
footer: { color: :light_cyan },
|
|
41
|
+
date: { color: :cyan, mode: :bold },
|
|
42
|
+
index: { color: :light_cyan },
|
|
43
|
+
# Status colors.
|
|
44
|
+
info: { color: :cyan },
|
|
45
|
+
success: { color: :green },
|
|
46
|
+
warning: { color: :yellow },
|
|
47
|
+
error: { color: :light_yellow, background: :red },
|
|
48
|
+
# Prompts
|
|
49
|
+
prompt: { color: :cyan, mode: :bold },
|
|
50
|
+
prompt_options: { color: :white, mode: :bold }
|
|
51
|
+
}.freeze
|
|
52
|
+
DEFAULT_THEME = {
|
|
53
|
+
version: VERSION,
|
|
54
|
+
description: 'Default theme.'
|
|
55
|
+
}.merge(DEFAULT_THEME_COLORS).freeze
|
|
56
|
+
|
|
57
|
+
# TODO: Validate other attrs.
|
|
58
|
+
validates_with Validators::DescriptionValidator
|
|
59
|
+
validates_with Validators::ColorThemeValidator
|
|
60
|
+
validates_with Validators::VersionValidator
|
|
61
|
+
|
|
62
|
+
attr_reader :theme_name, :options
|
|
63
|
+
|
|
64
|
+
def initialize(theme_name:, theme_hash: nil, options: {})
|
|
65
|
+
raise ArgumentError, 'theme_name is nil.' if theme_name.nil?
|
|
66
|
+
raise ArgumentError, "theme_name is the wrong object type: \"#{theme_name}\"." unless theme_name.is_a?(String)
|
|
67
|
+
unless theme_hash.is_a?(Hash) || theme_hash.nil?
|
|
68
|
+
raise ArgumentError, "theme_hash is the wrong object type: \"#{theme_hash}\"."
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
FileUtils.mkdir_p themes_folder
|
|
72
|
+
|
|
73
|
+
@theme_name = theme_name
|
|
74
|
+
@options = options || {}
|
|
75
|
+
|
|
76
|
+
super(self.class.send(:themes_path_for, theme_name: @theme_name))
|
|
77
|
+
|
|
78
|
+
theme_hash ||= DEFAULT_THEME.merge(description: "#{@theme_name.capitalize} theme")
|
|
79
|
+
|
|
80
|
+
# Color themes I expect will change a lot, so we're using
|
|
81
|
+
# a little meta-programming here to dynamically create
|
|
82
|
+
# public attr_readers and private attr_writers based on the
|
|
83
|
+
# keys in DEFAULT_THEME, then assign those attributes from
|
|
84
|
+
# the values in theme_hash. theme_hash will be guaranteed to
|
|
85
|
+
# have the same keys as DEFAULT_THEME.keys at this point
|
|
86
|
+
# because we called ensure_theme_hash! above.
|
|
87
|
+
DEFAULT_THEME.each_key do |attr|
|
|
88
|
+
self.class.class_eval do
|
|
89
|
+
attr_reader attr
|
|
90
|
+
attr_writer attr
|
|
91
|
+
private "#{attr}="
|
|
92
|
+
end
|
|
93
|
+
attr_value = theme_hash[attr]
|
|
94
|
+
attr_value = attr_value.merge_default_colors if default_theme_color_keys.include?(attr)
|
|
95
|
+
send("#{attr}=", attr_value)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def delete
|
|
100
|
+
self.class.delete(theme_name: theme_name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def delete!
|
|
104
|
+
self.class.delete!(theme_name: theme_name)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def exist?
|
|
108
|
+
self.class.exist?(theme_name: theme_name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
class << self
|
|
112
|
+
def all
|
|
113
|
+
Dir.glob("#{themes_folder}/*").map do |file_path|
|
|
114
|
+
theme_name = File.basename(file_path, '.*')
|
|
115
|
+
find(theme_name: theme_name)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def configuration
|
|
120
|
+
Models::Configuration.new
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def current
|
|
124
|
+
theme_name = configuration.theme_name
|
|
125
|
+
return unless exist?(theme_name: theme_name)
|
|
126
|
+
|
|
127
|
+
find(theme_name: theme_name)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Returns the current color theme if it exists; otherwise,
|
|
131
|
+
# it returns the default color theme.
|
|
132
|
+
def current_or_default
|
|
133
|
+
current || default
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def default
|
|
137
|
+
new(theme_name: DEFAULT_THEME_NAME, theme_hash: DEFAULT_THEME)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def delete(theme_name:)
|
|
141
|
+
superclass.delete(file_path: themes_path_for(theme_name: theme_name))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def delete!(theme_name:)
|
|
145
|
+
superclass.delete!(file_path: themes_path_for(theme_name: theme_name))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ensure_color_theme_color_defaults_for(theme_hash: DEFAULT_THEME)
|
|
149
|
+
theme_hash = theme_hash.dup
|
|
150
|
+
|
|
151
|
+
theme_hash.each_pair do |key, value|
|
|
152
|
+
next unless default_theme_color_keys.include?(key)
|
|
153
|
+
|
|
154
|
+
theme_hash[key] = value.merge_default_colors
|
|
155
|
+
end
|
|
156
|
+
theme_hash
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def exist?(theme_name:)
|
|
160
|
+
superclass.exist?(file_path: themes_path_for(theme_name: theme_name))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def find(theme_name:)
|
|
164
|
+
theme_hash = read!(file_path: themes_path_for(theme_name: theme_name))
|
|
165
|
+
Services::ColorTheme::HydratorService.new(theme_name: theme_name, theme_hash: theme_hash).call
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def find_or_create(theme_name:)
|
|
169
|
+
return find(theme_name: theme_name) if exist?(theme_name: theme_name)
|
|
170
|
+
|
|
171
|
+
new(theme_name: theme_name).tap(&:write!)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def find_or_initialize(theme_name:)
|
|
175
|
+
return find(theme_name: theme_name) if exist?(theme_name: theme_name)
|
|
176
|
+
|
|
177
|
+
new(theme_name: theme_name)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# TODO: Unused?
|
|
181
|
+
# def build_color_theme(theme_name:, base_color:, description:)
|
|
182
|
+
# theme_hash = Models::ColorTheme.send(:replace, color_theme: default,
|
|
183
|
+
# replace_color: :cyan, with_color: base_color).tap do |hash|
|
|
184
|
+
# hash[:description] = description
|
|
185
|
+
# end
|
|
186
|
+
# new(theme_name: theme_name, theme_hash: theme_hash)
|
|
187
|
+
# end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def default_theme_color_keys
|
|
192
|
+
DEFAULT_THEME_COLORS.keys
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def replace(color_theme:, replace_color:, with_color:)
|
|
196
|
+
colors_theme_hash = color_theme.to_theme_colors_h.tap do |hash|
|
|
197
|
+
hash.each_key do |key|
|
|
198
|
+
hash[key] = replace_color(theme_color: hash[key],
|
|
199
|
+
replace_color: replace_color, with_color: with_color)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
DEFAULT_THEME.merge(colors_theme_hash)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def replace_color(theme_color:, replace_color:, with_color:)
|
|
206
|
+
%i[color background].each do |color_type|
|
|
207
|
+
color = theme_color[color_type].to_s.sub(replace_color.to_s, with_color.to_s)
|
|
208
|
+
theme_color[color_type] = color.sub('light_light_', 'light_').to_sym
|
|
209
|
+
end
|
|
210
|
+
theme_color
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# If the color theme is deleted (deleted_theme_name) and the current
|
|
214
|
+
# theme_name in the configuration is the same as the deleted theme,
|
|
215
|
+
# we need to reset the configuration theme to the default theme.
|
|
216
|
+
def reset_default_configuration_color_theme_if!(deleted_theme_name:)
|
|
217
|
+
config = configuration
|
|
218
|
+
return if config.theme_name == self::DEFAULT_THEME_NAME
|
|
219
|
+
return unless config.theme_name == deleted_theme_name
|
|
220
|
+
return unless config.exist?
|
|
221
|
+
|
|
222
|
+
config.theme_name = self::DEFAULT_THEME_NAME
|
|
223
|
+
config.write!
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def themes_path_for(theme_name:)
|
|
227
|
+
Support::Fileable.themes_path(theme_name: theme_name)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def to_h
|
|
232
|
+
{}.tap do |hash|
|
|
233
|
+
DEFAULT_THEME.each_key do |key|
|
|
234
|
+
hash[key] = public_send(key)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def to_theme_colors_h
|
|
240
|
+
{}.tap do |hash|
|
|
241
|
+
DEFAULT_THEME_COLORS.each_key do |key|
|
|
242
|
+
hash[key] = public_send(key)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def ==(other)
|
|
248
|
+
return false unless other.is_a?(self.class)
|
|
249
|
+
return false unless other.theme_name == theme_name
|
|
250
|
+
|
|
251
|
+
DEFAULT_THEME.keys.all? { |key| public_send(key) == other.public_send(key) }
|
|
252
|
+
end
|
|
253
|
+
alias eql? ==
|
|
254
|
+
|
|
255
|
+
def hash
|
|
256
|
+
DEFAULT_THEME.keys.map { |key| public_send(key) }.tap do |hashes|
|
|
257
|
+
hashes << theme_name.hash
|
|
258
|
+
end.hash
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
private
|
|
262
|
+
|
|
263
|
+
attr_writer :theme_name, :description
|
|
264
|
+
|
|
265
|
+
def default_theme_color_keys
|
|
266
|
+
@default_theme_color_keys ||= self.class.send(:default_theme_color_keys)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
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 Dsu
|
|
11
|
+
module Models
|
|
12
|
+
# This class represents the dsu 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 dsu commands that list date ranges (e.g.
|
|
31
|
+
# `dsu list dates`), the displayed list will include dates that
|
|
32
|
+
# have no dsu entries. If false, the displayed list will only
|
|
33
|
+
# include dates that have dsu entries.
|
|
34
|
+
# For all other `dsu 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 DSU date has entries or not;
|
|
38
|
+
# all other dates will not be displayed if the DSU 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
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
validates_with Validators::VersionValidator
|
|
49
|
+
validates :editor, presence: true
|
|
50
|
+
validates :entries_display_order, presence: true,
|
|
51
|
+
inclusion: { in: %i[asc desc], message: 'must be :asc or :desc' }
|
|
52
|
+
validates :carry_over_entries_to_today, inclusion: { in: [true, false], message: 'must be true or false' }
|
|
53
|
+
validates :include_all, inclusion: { in: [true, false], message: 'must be true or false' }
|
|
54
|
+
validates :theme_name, presence: true
|
|
55
|
+
validate :validate_theme_file
|
|
56
|
+
|
|
57
|
+
attr_accessor :version,
|
|
58
|
+
:editor,
|
|
59
|
+
:entries_display_order,
|
|
60
|
+
:carry_over_entries_to_today,
|
|
61
|
+
:include_all,
|
|
62
|
+
:theme_name
|
|
63
|
+
|
|
64
|
+
attr_reader :options
|
|
65
|
+
|
|
66
|
+
def initialize(options: {})
|
|
67
|
+
super(config_path)
|
|
68
|
+
|
|
69
|
+
FileUtils.mkdir_p config_folder
|
|
70
|
+
|
|
71
|
+
@options = options || {}
|
|
72
|
+
reload
|
|
73
|
+
|
|
74
|
+
write! unless exist?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Temporarily sets the configuration to the given config_hash.
|
|
78
|
+
# To reset the configuration to its original state, call #reload
|
|
79
|
+
def replace!(config_hash: {})
|
|
80
|
+
raise ArgumentError, 'config_hash is nil.' if config_hash.nil?
|
|
81
|
+
raise ArgumentError, "config_hash must be a Hash: \"#{config_hash}\"." unless config_hash.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
assign_attributes_from config_hash.dup
|
|
84
|
+
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Restores the configuration to its original state from disk.
|
|
89
|
+
def reload
|
|
90
|
+
file_hash = if exist?
|
|
91
|
+
read do |config_hash|
|
|
92
|
+
hydrated_hash = Services::Configuration::HydratorService.new(config_hash: config_hash).call
|
|
93
|
+
config_hash.merge!(hydrated_hash)
|
|
94
|
+
end
|
|
95
|
+
else
|
|
96
|
+
DEFAULT_CONFIGURATION.dup
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
assign_attributes_from file_hash
|
|
100
|
+
|
|
101
|
+
self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def carry_over_entries_to_today?
|
|
105
|
+
carry_over_entries_to_today
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def to_h
|
|
109
|
+
{
|
|
110
|
+
version: version,
|
|
111
|
+
editor: editor,
|
|
112
|
+
entries_display_order: entries_display_order,
|
|
113
|
+
carry_over_entries_to_today: carry_over_entries_to_today,
|
|
114
|
+
include_all: include_all,
|
|
115
|
+
theme_name: theme_name
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Override == and hash so that we can compare objects based
|
|
120
|
+
# on attributes alone. This is also useful for comparing objects
|
|
121
|
+
# in an array, for example.
|
|
122
|
+
def ==(other)
|
|
123
|
+
return false unless other.is_a?(Configuration)
|
|
124
|
+
|
|
125
|
+
to_h == other.to_h
|
|
126
|
+
end
|
|
127
|
+
alias eql? ==
|
|
128
|
+
|
|
129
|
+
def hash
|
|
130
|
+
DEFAULT_CONFIGURATION.each_key.map do |key|
|
|
131
|
+
public_send(key)
|
|
132
|
+
end.hash
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def merge(hash)
|
|
136
|
+
replace!(config_hash: to_h.merge(hash))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
private
|
|
140
|
+
|
|
141
|
+
def assign_attributes_from(config_hash)
|
|
142
|
+
@version = config_hash.fetch(:version, VERSION)
|
|
143
|
+
@editor = config_hash.fetch(:editor, DEFAULT_CONFIGURATION[:editor])
|
|
144
|
+
@entries_display_order = config_hash.fetch(:entries_display_order,
|
|
145
|
+
DEFAULT_CONFIGURATION[:entries_display_order])
|
|
146
|
+
@carry_over_entries_to_today = config_hash.fetch(:carry_over_entries_to_today,
|
|
147
|
+
DEFAULT_CONFIGURATION[:carry_over_entries_to_today])
|
|
148
|
+
@include_all = config_hash.fetch(:include_all, DEFAULT_CONFIGURATION[:include_all])
|
|
149
|
+
@theme_name = config_hash.fetch(:theme_name, DEFAULT_CONFIGURATION[:theme_name])
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def validate_theme_file
|
|
153
|
+
theme_path = themes_path(theme_name: theme_name)
|
|
154
|
+
return if File.exist?(theme_path)
|
|
155
|
+
|
|
156
|
+
errors.add(:base, "Theme file \"#{theme_path}\" does not exist")
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
data/lib/dsu/models/entry.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_model'
|
|
4
4
|
require_relative '../support/descriptable'
|
|
5
|
+
require_relative '../support/presentable'
|
|
5
6
|
require_relative '../validators/description_validator'
|
|
6
7
|
|
|
7
8
|
module Dsu
|
|
@@ -11,15 +12,18 @@ module Dsu
|
|
|
11
12
|
class Entry
|
|
12
13
|
include ActiveModel::Model
|
|
13
14
|
include Support::Descriptable
|
|
15
|
+
include Support::Presentable
|
|
14
16
|
|
|
15
17
|
validates_with Validators::DescriptionValidator
|
|
16
18
|
|
|
17
|
-
attr_reader :description
|
|
19
|
+
attr_reader :description, :options
|
|
18
20
|
|
|
19
|
-
def initialize(description:)
|
|
21
|
+
def initialize(description:, options: {})
|
|
20
22
|
raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
|
|
21
23
|
|
|
24
|
+
# Make sure to call the setter method so that the description is cleaned up.
|
|
22
25
|
self.description = description
|
|
26
|
+
@options = options || {}
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
class << self
|
|
@@ -1,64 +1,76 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_model'
|
|
4
|
-
require_relative '../
|
|
5
|
-
require_relative '../
|
|
6
|
-
require_relative '../services/
|
|
7
|
-
require_relative '../
|
|
8
|
-
require_relative '../support/
|
|
4
|
+
require_relative '../crud/json_file'
|
|
5
|
+
require_relative '../migration/version'
|
|
6
|
+
require_relative '../services/entry_group/editor_service'
|
|
7
|
+
require_relative '../support/fileable'
|
|
8
|
+
require_relative '../support/presentable'
|
|
9
|
+
require_relative '../support/time_comparable'
|
|
9
10
|
require_relative '../support/time_formatable'
|
|
10
11
|
require_relative '../validators/entries_validator'
|
|
11
12
|
require_relative '../validators/time_validator'
|
|
13
|
+
require_relative '../validators/version_validator'
|
|
12
14
|
require_relative 'entry'
|
|
13
15
|
|
|
14
16
|
module Dsu
|
|
15
17
|
module Models
|
|
16
18
|
# This class represents a group of entries for a given day. IOW,
|
|
17
19
|
# things someone might want to share at their daily standup (DSU).
|
|
18
|
-
class EntryGroup
|
|
19
|
-
|
|
20
|
-
include
|
|
20
|
+
class EntryGroup < Crud::JsonFile
|
|
21
|
+
include Support::Fileable
|
|
22
|
+
include Support::Presentable
|
|
23
|
+
include Support::TimeComparable
|
|
21
24
|
include Support::TimeFormatable
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
ENTRIES_FILE_NAME_REGEX = /\d{4}-\d{2}-\d{2}.json/
|
|
27
|
+
ENTRIES_FILE_NAME_TIME_REGEX = /\d{4}-\d{2}-\d{2}/
|
|
28
|
+
VERSION = Migration::VERSION
|
|
29
|
+
|
|
30
|
+
attr_accessor :time, :version
|
|
31
|
+
attr_reader :entries, :options
|
|
25
32
|
|
|
26
33
|
validates_with Validators::EntriesValidator
|
|
27
34
|
validates_with Validators::TimeValidator
|
|
35
|
+
validates_with Validators::VersionValidator
|
|
28
36
|
|
|
29
|
-
def initialize(time: nil, entries:
|
|
37
|
+
def initialize(time: nil, entries: nil, version: nil, options: {})
|
|
30
38
|
raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
|
|
39
|
+
raise ArgumentError, 'version is the wrong object type' unless version.is_a?(Integer) || version.nil?
|
|
40
|
+
|
|
41
|
+
FileUtils.mkdir_p(entries_folder)
|
|
31
42
|
|
|
32
43
|
@time = ensure_local_time(time)
|
|
44
|
+
|
|
45
|
+
super(entries_path(time: @time))
|
|
46
|
+
|
|
47
|
+
@version = version || VERSION
|
|
33
48
|
self.entries = entries || []
|
|
49
|
+
@options = options || {}
|
|
34
50
|
end
|
|
35
51
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def edit(time:, options: {})
|
|
42
|
-
# NOTE: Uncomment this line to prohibit edits on
|
|
43
|
-
# Entry Groups that do not exist (i.e. have no entries).
|
|
44
|
-
# return new(time: time) unless exists?(time: time)
|
|
52
|
+
# Override == and hash so that we can compare Entry Group objects.
|
|
53
|
+
def ==(other)
|
|
54
|
+
return false unless other.is_a?(EntryGroup) &&
|
|
55
|
+
version == other.version &&
|
|
56
|
+
time_equal?(other_time: other.time)
|
|
45
57
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
end
|
|
58
|
+
entries == other.entries
|
|
59
|
+
end
|
|
60
|
+
alias eql? ==
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
end
|
|
62
|
+
def clone
|
|
63
|
+
self.class.new(time: time, entries: entries.map(&:clone), version: version)
|
|
54
64
|
end
|
|
55
65
|
|
|
56
|
-
def
|
|
57
|
-
|
|
66
|
+
def delete
|
|
67
|
+
self.class.delete(time: time)
|
|
68
|
+
entries.clear
|
|
58
69
|
end
|
|
59
70
|
|
|
60
|
-
def
|
|
61
|
-
self.class.
|
|
71
|
+
def delete!
|
|
72
|
+
self.class.delete!(time: time)
|
|
73
|
+
entries.clear
|
|
62
74
|
end
|
|
63
75
|
|
|
64
76
|
def entries=(entries)
|
|
@@ -70,32 +82,121 @@ module Dsu
|
|
|
70
82
|
@entries = entries.map(&:clone)
|
|
71
83
|
end
|
|
72
84
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
def exist?
|
|
86
|
+
self.class.exist?(time: time)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def hash
|
|
90
|
+
entries.map(&:hash).tap do |hashes|
|
|
91
|
+
hashes << version.hash
|
|
92
|
+
hashes << time_equal_compare_string_for(time: time)
|
|
93
|
+
end.hash
|
|
78
94
|
end
|
|
79
95
|
|
|
80
96
|
def time_formatted
|
|
81
97
|
formatted_time(time: time)
|
|
82
98
|
end
|
|
83
99
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
validate!
|
|
88
|
-
Services::EntryGroupWriterService.new(entry_group: self).call
|
|
89
|
-
self
|
|
100
|
+
def time_yyyy_mm_dd
|
|
101
|
+
yyyy_mm_dd(time: time)
|
|
90
102
|
end
|
|
91
103
|
|
|
92
104
|
def to_h
|
|
93
105
|
{
|
|
106
|
+
version: version,
|
|
94
107
|
time: time.dup,
|
|
95
108
|
entries: entries.map(&:to_h)
|
|
96
109
|
}
|
|
97
110
|
end
|
|
98
111
|
|
|
112
|
+
def valid_unique_entries
|
|
113
|
+
entries&.select(&:valid?)&.uniq(&:description)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class << self
|
|
117
|
+
def all
|
|
118
|
+
entry_files.filter_map do |file_path|
|
|
119
|
+
entry_file_name = File.basename(file_path)
|
|
120
|
+
next unless entry_file_name.match?(ENTRIES_FILE_NAME_REGEX)
|
|
121
|
+
|
|
122
|
+
entry_date = File.basename(entry_file_name, '.*')
|
|
123
|
+
file_path = entries_path_for(time: Time.parse(entry_date))
|
|
124
|
+
entry_group_hash = read!(file_path: file_path)
|
|
125
|
+
Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def any?
|
|
130
|
+
entry_files.any? do |file_path|
|
|
131
|
+
entry_date = File.basename(file_path, '.*')
|
|
132
|
+
entry_date.match?(ENTRIES_FILE_NAME_TIME_REGEX)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def delete(time:)
|
|
137
|
+
superclass.delete(file_path: entries_path_for(time: time))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def delete!(time:)
|
|
141
|
+
superclass.delete!(file_path: entries_path_for(time: time))
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def edit(time:, options: {})
|
|
145
|
+
# NOTE: Uncomment this line to prohibit edits on
|
|
146
|
+
# Entry Groups that do not exist (i.e. have no entries).
|
|
147
|
+
# return new(time: time) unless exists?(time: time)
|
|
148
|
+
|
|
149
|
+
find_or_initialize(time: time).tap do |entry_group|
|
|
150
|
+
Services::EntryGroup::EditorService.new(entry_group: entry_group, options: options).call
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def exist?(time:)
|
|
155
|
+
superclass.exist?(file_path: entries_path_for(time: time))
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def find(time:)
|
|
159
|
+
file_path = entries_path_for(time: time)
|
|
160
|
+
entry_group_hash = read!(file_path: file_path)
|
|
161
|
+
Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def find_or_create(time:)
|
|
165
|
+
find_or_initialize(time: time).tap do |entry_group|
|
|
166
|
+
entry_group.write! unless entry_group.exist?
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def find_or_initialize(time:)
|
|
171
|
+
file_path = entries_path_for(time: time)
|
|
172
|
+
read(file_path: file_path) do |entry_group_hash|
|
|
173
|
+
Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
|
|
174
|
+
end || new(time: time)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def write(file_data:, file_path:)
|
|
178
|
+
delete(file_path: file_path) and return true if file_data[:entries].empty?
|
|
179
|
+
|
|
180
|
+
super
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def write!(file_data:, file_path:)
|
|
184
|
+
delete!(file_path: file_path) and return if file_data[:entries].empty?
|
|
185
|
+
|
|
186
|
+
super
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def entries_path_for(time:)
|
|
192
|
+
Support::Fileable.entries_path(time: time)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def entry_files
|
|
196
|
+
Dir.glob("#{entries_folder}/*")
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
99
200
|
private
|
|
100
201
|
|
|
101
202
|
def ensure_local_time(time)
|