dsu 2.4.3 → 3.0.0.alpha.0

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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/CHANGELOG.md +181 -204
  4. data/Gemfile.lock +13 -13
  5. data/README.md +7 -8
  6. data/Rakefile +6 -0
  7. data/current_project.bak +4 -0
  8. data/lib/dsu/cli.rb +24 -6
  9. data/lib/dsu/crud/json_file.rb +3 -0
  10. data/lib/dsu/migration/version.rb +1 -1
  11. data/lib/dsu/models/color_theme.rb +7 -58
  12. data/lib/dsu/models/configuration.rb +18 -3
  13. data/lib/dsu/models/entry_group.rb +0 -7
  14. data/lib/dsu/models/migration_version.rb +0 -1
  15. data/lib/dsu/models/project.rb +295 -0
  16. data/lib/dsu/presenters/base_presenter_ex.rb +1 -12
  17. data/lib/dsu/presenters/export/all_presenter.rb +14 -19
  18. data/lib/dsu/presenters/export/dates_presenter.rb +17 -20
  19. data/lib/dsu/presenters/import/all_presenter.rb +20 -25
  20. data/lib/dsu/presenters/import/dates_presenter.rb +25 -27
  21. data/lib/dsu/presenters/import/import_entry.rb +22 -0
  22. data/lib/dsu/presenters/import/import_file.rb +9 -1
  23. data/lib/dsu/presenters/project/create_presenter.rb +44 -0
  24. data/lib/dsu/presenters/project/delete_by_number_presenter.rb +54 -0
  25. data/lib/dsu/presenters/project/delete_presenter.rb +53 -0
  26. data/lib/dsu/presenters/project/list_presenter.rb +24 -0
  27. data/lib/dsu/presenters/project/rename_by_number_presenter.rb +63 -0
  28. data/lib/dsu/presenters/project/rename_presenter.rb +57 -0
  29. data/lib/dsu/presenters/project/use_by_number_presenter.rb +53 -0
  30. data/lib/dsu/presenters/project/use_presenter.rb +52 -0
  31. data/lib/dsu/services/entry_group/exporter_service.rb +22 -5
  32. data/lib/dsu/services/entry_group/importer_service.rb +41 -8
  33. data/lib/dsu/services/project/hydrator_service.rb +40 -0
  34. data/lib/dsu/services/project/rename_service.rb +70 -0
  35. data/lib/dsu/subcommands/export.rb +4 -2
  36. data/lib/dsu/subcommands/import.rb +7 -3
  37. data/lib/dsu/subcommands/project.rb +149 -0
  38. data/lib/dsu/support/ask.rb +10 -3
  39. data/lib/dsu/support/color_themable.rb +1 -1
  40. data/lib/dsu/support/command_hookable.rb +7 -2
  41. data/lib/dsu/support/descriptable.rb +5 -21
  42. data/lib/dsu/support/fileable.rb +39 -1
  43. data/lib/dsu/support/project_file_system.rb +121 -0
  44. data/lib/dsu/support/short_string.rb +24 -0
  45. data/lib/dsu/support/time_comparable.rb +2 -0
  46. data/lib/dsu/support/transform_project_name.rb +24 -0
  47. data/lib/dsu/validators/project_name_validator.rb +58 -0
  48. data/lib/dsu/version.rb +1 -1
  49. data/lib/dsu/views/base_list_view.rb +41 -0
  50. data/lib/dsu/views/export.rb +60 -6
  51. data/lib/dsu/views/import.rb +83 -7
  52. data/lib/dsu/views/import_dates.rb +17 -0
  53. data/lib/dsu/views/project/create.rb +87 -0
  54. data/lib/dsu/views/project/delete.rb +96 -0
  55. data/lib/dsu/views/project/delete_by_number.rb +19 -0
  56. data/lib/dsu/views/project/list.rb +115 -0
  57. data/lib/dsu/views/project/rename.rb +98 -0
  58. data/lib/dsu/views/project/rename_by_number.rb +21 -0
  59. data/lib/dsu/views/project/use.rb +97 -0
  60. data/lib/dsu/views/project/use_by_number.rb +19 -0
  61. data/lib/dsu.rb +2 -10
  62. data/lib/locales/en/active_record.yml +9 -0
  63. data/lib/locales/en/commands.yml +9 -3
  64. data/lib/locales/en/miscellaneous.yml +4 -0
  65. data/lib/locales/en/services.yml +4 -0
  66. data/lib/locales/en/subcommands.yml +247 -15
  67. data/project.bak +0 -0
  68. metadata +34 -9
  69. data/lib/dsu/presenters/export/messages.rb +0 -32
  70. data/lib/dsu/presenters/export/nothing_to_export.rb +0 -13
  71. data/lib/dsu/presenters/export/service_callable.rb +0 -20
  72. data/lib/dsu/presenters/import/messages.rb +0 -42
  73. data/lib/dsu/presenters/import/service_callable.rb +0 -21
data/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # `dsu`
2
2
 
3
3
  [![Ruby](https://github.com/gangelo/dsu/actions/workflows/ruby.yml/badge.svg)](https://github.com/gangelo/dsu/actions/workflows/ruby.yml)
4
- [![GitHub version](http://badge.fury.io/gh/gangelo%2Fdsu.svg?refresh=9)](https://badge.fury.io/gh/gangelo%2Fdsu)
5
- [![Gem Version](https://badge.fury.io/rb/dsu.svg?refresh=9)](https://badge.fury.io/rb/dsu)
4
+ [![GitHub version](http://badge.fury.io/gh/gangelo%2Fdsu.svg?refresh=12)](https://badge.fury.io/gh/gangelo%2Fdsu)
5
+ [![Gem Version](https://badge.fury.io/rb/dsu.svg?refresh=12)](https://badge.fury.io/rb/dsu)
6
6
  [![Documentation](http://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/gems/dsu/)
7
7
  [![Report Issues](https://img.shields.io/badge/report-issues-red.svg)](https://github.com/gangelo/dsu/issues)
8
8
  [![License](http://img.shields.io/badge/license-MIT-yellowgreen.svg)](#license)
9
9
 
10
- `dsu` is a [ruby gem](https://rubygems.org/gems/dsu) that enables anyone practicing [Agile methodology](https://www.agilealliance.org/agile101/) to record, keep track of and manage their [daily standup (DSU)](https://www.agilealliance.org/glossary/daily-meeting/) activities.
10
+ `dsu` is a [ruby gem](https://rubygems.org/gems/dsu) that enables anyone practicing the [Agile methodology](https://www.agilealliance.org/agile101/) to record, keep track of and manage their [daily standup (DSU)](https://www.agilealliance.org/glossary/daily-meeting/) activities.
11
11
 
12
12
  - `dsu` uses _no_ network connections whatsoever.
13
13
  - `dsu` stores all of its data _locally_, in .json files.
@@ -22,12 +22,11 @@ gem install dsu
22
22
  ```
23
23
 
24
24
  # Documentation
25
- [https://github.com/gangelo/dsu/wiki](https://github.com/gangelo/dsu/wiki)
25
+ The [dsu wiki](https://github.com/gangelo/dsu/wiki) is currently the gold standard for `dsu` documentation.
26
26
 
27
27
  # Examples
28
- The [dsu wiki](https://github.com/gangelo/dsu/wiki) is repleat with practical examples on how to use `dsu`.
29
-
30
- It you're interested in how _I personally_ use `dsu` every day, I blog about it [here](https://genemangelojr.blogspot.com/2024/01/the-dsu-ruby-gem-workflow-how-to-use-it.html).
28
+ * The [dsu wiki](https://github.com/gangelo/dsu/wiki) is repleat with practical examples on how to use `dsu`.
29
+ * Visit the [How I use dsu daily as an Agile developer](https://github.com/gangelo/dsu/wiki/How-I-use-dsu-daily-as-an-Agile-developer) wiki for examples of how _I_ use `dsu` on a daily basis.
31
30
 
32
31
  # Supported ruby versions
33
32
  `dsu` _should_ work with any ruby version `['>= 3.0.1', '< 4.0']`; however, `dsu` is currently tested against the following ruby versions:
@@ -36,4 +35,4 @@ It you're interested in how _I personally_ use `dsu` every day, I blog about it
36
35
  - 3.1.4
37
36
  - 3.2.2
38
37
 
39
- Copyright (c) 2023-2024 Erik Gene Angelo. See [LICENSE](https://github.com/gangelo/dsu/blob/main/LICENSE.txt) for details.
38
+ Copyright (c) 2023-2024 Gene Angelo. See [LICENSE](https://github.com/gangelo/dsu/blob/main/LICENSE.txt) for details.
data/Rakefile CHANGED
@@ -10,3 +10,9 @@ require "rubocop/rake_task"
10
10
  RuboCop::RakeTask.new
11
11
 
12
12
  task default: %i[spec rubocop]
13
+
14
+ desc 'Generate a migration timestamp'
15
+ task :timestamp do
16
+ puts 'The below migration timestamp should be placed in the "lib/dsu/migration/version.rb" file.'
17
+ puts Time.now.strftime('%Y%m%d%H%M%S')
18
+ end
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": 20240210161248,
3
+ "project_name": "default"
4
+ }
data/lib/dsu/cli.rb CHANGED
@@ -11,12 +11,18 @@ require_relative 'subcommands/edit'
11
11
  require_relative 'subcommands/export'
12
12
  require_relative 'subcommands/import'
13
13
  require_relative 'subcommands/list'
14
+ require_relative 'subcommands/project'
14
15
  require_relative 'subcommands/theme'
16
+ require_relative 'support/command_options/time_mnemonic'
17
+ require_relative 'support/time_formatable'
15
18
  require_relative 'views/entry_group/list'
16
19
 
17
20
  module Dsu
18
21
  # The `dsu` command.
19
22
  class CLI < BaseCLI
23
+ include Support::CommandOptions::TimeMnemonic
24
+ include Support::TimeFormatable
25
+
20
26
  map I18n.t('commands.add.key_mappings') => :add
21
27
  map I18n.t('commands.browse.key_mappings') => :browse
22
28
  map I18n.t('commands.config.key_mappings') => :config
@@ -27,30 +33,39 @@ module Dsu
27
33
  map I18n.t('commands.import.key_mappings') => :import
28
34
  map I18n.t('commands.info.key_mappings') => :info
29
35
  map I18n.t('commands.list.key_mappings') => :list
36
+ map I18n.t('commands.project.key_mappings') => :project
30
37
  map I18n.t('commands.theme.key_mappings') => :theme
31
38
  map I18n.t('commands.version.key_mappings') => :version
32
39
 
33
40
  desc I18n.t('commands.add.desc'), I18n.t('commands.add.usage')
34
- long_desc I18n.t('commands.add.long_desc', date_option_description: date_option_description)
41
+ long_desc I18n.t('commands.add.long_desc',
42
+ date_option_description: date_option_description, mnemonic_option_description: mnemonic_option_description)
35
43
  option I18n.t('options.date.name'), aliases: I18n.t('options.date.aliases'), type: :string
36
44
  option I18n.t('options.tomorrow.name'), aliases: I18n.t('options.tomorrow.aliases'), type: :boolean
37
45
  option I18n.t('options.yesterday.name'), aliases: I18n.t('options.yesterday.aliases'), type: :boolean
38
46
  option I18n.t('options.today.name'), aliases: I18n.t('options.today.aliases'), type: :boolean, default: true
39
47
  def add(description)
40
- time = if options[I18n.t('options.date.name')].present?
41
- Time.parse(options[I18n.t('options.date.name')])
48
+ date_or_mnemonic = if options[I18n.t('options.date.name')].present?
49
+ options[I18n.t('options.date.name')]
42
50
  elsif options[I18n.t('options.tomorrow.name')].present?
43
- Time.now.tomorrow
51
+ I18n.t('options.tomorrow.name')
44
52
  elsif options[I18n.t('options.yesterday.name')].present?
45
- Time.now.yesterday
53
+ I18n.t('options.yesterday.name')
46
54
  elsif options[I18n.t('options.today.name')].present?
47
- Time.now
55
+ I18n.t('options.today.name')
56
+ end
57
+ time = if time_mnemonic?(date_or_mnemonic)
58
+ time_from_mnemonic(command_option: date_or_mnemonic)
59
+ else
60
+ Time.parse(date_or_mnemonic)
48
61
  end
49
62
  entry = Models::Entry.new(description: description)
50
63
  CommandServices::AddEntryService.new(entry: entry, time: time).call
51
64
  presenter = Presenters::EntryGroup::List::DatePresenter.new(times: [time], options: options)
52
65
  # TODO: Refactor View::EntryGroup::Show to accept a presenter and use it here
53
66
  Views::EntryGroup::List.new(presenter: presenter).render
67
+ rescue ArgumentError => e
68
+ Views::Shared::Error.new(messages: e.message).render
54
69
  end
55
70
 
56
71
  desc I18n.t('commands.browse.desc'), I18n.t('commands.browse.usage')
@@ -59,6 +74,9 @@ module Dsu
59
74
  desc I18n.t('commands.list.desc'), I18n.t('commands.list.usage')
60
75
  subcommand :list, Subcommands::List
61
76
 
77
+ desc I18n.t('commands.project.desc'), I18n.t('commands.project.usage')
78
+ subcommand :project, Subcommands::Project
79
+
62
80
  desc I18n.t('commands.config.desc'), I18n.t('commands.config.usage')
63
81
  subcommand :config, Subcommands::Config
64
82
 
@@ -114,6 +114,9 @@ module Dsu
114
114
  end
115
115
 
116
116
  def write!(file_data:, file_path:)
117
+ # TODO: Should we be raising an error if the file does not exist?
118
+ # raise file_does_not_exist_message(file_path: file_path) unless file_exist?(file_path: file_path)
119
+
117
120
  write(file_data: file_data, file_path: file_path)
118
121
  end
119
122
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Dsu
4
4
  module Migration
5
- VERSION = 20230613121411 # rubocop:disable Style/NumericLiterals
5
+ VERSION = 20240210161248 # rubocop:disable Style/NumericLiterals
6
6
  end
7
7
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
3
  require_relative '../crud/json_file'
5
4
  require_relative '../migration/version'
6
5
  require_relative '../support/color_themable'
@@ -76,7 +75,7 @@ module Dsu
76
75
  @theme_name = theme_name
77
76
  @options = options || {}
78
77
 
79
- super(self.class.send(:themes_path_for, theme_name: @theme_name))
78
+ super(self.class.send(:themes_path, theme_name: @theme_name))
80
79
 
81
80
  theme_hash ||= DEFAULT_THEME.merge(description: "#{@theme_name.capitalize} theme")
82
81
 
@@ -112,6 +111,8 @@ module Dsu
112
111
  end
113
112
 
114
113
  class << self
114
+ delegate :themes_folder, :themes_path, to: Support::Fileable
115
+
115
116
  def all
116
117
  Dir.glob("#{themes_folder}/*").map do |file_path|
117
118
  theme_name = File.basename(file_path, '.*')
@@ -141,11 +142,11 @@ module Dsu
141
142
  end
142
143
 
143
144
  def delete(theme_name:)
144
- superclass.delete(file_path: themes_path_for(theme_name: theme_name))
145
+ superclass.delete(file_path: themes_path(theme_name: theme_name))
145
146
  end
146
147
 
147
148
  def delete!(theme_name:)
148
- superclass.delete!(file_path: themes_path_for(theme_name: theme_name))
149
+ superclass.delete!(file_path: themes_path(theme_name: theme_name))
149
150
  end
150
151
 
151
152
  def ensure_color_theme_color_defaults_for(theme_hash: DEFAULT_THEME)
@@ -160,11 +161,11 @@ module Dsu
160
161
  end
161
162
 
162
163
  def exist?(theme_name:)
163
- superclass.file_exist?(file_path: themes_path_for(theme_name: theme_name))
164
+ superclass.file_exist?(file_path: themes_path(theme_name: theme_name))
164
165
  end
165
166
 
166
167
  def find(theme_name:)
167
- theme_hash = read!(file_path: themes_path_for(theme_name: theme_name))
168
+ theme_hash = read!(file_path: themes_path(theme_name: theme_name))
168
169
  Services::ColorTheme::HydratorService.new(theme_name: theme_name, theme_hash: theme_hash).call
169
170
  end
170
171
 
@@ -180,55 +181,11 @@ module Dsu
180
181
  new(theme_name: theme_name)
181
182
  end
182
183
 
183
- # TODO: Unused?
184
- # def build_color_theme(theme_name:, base_color:, description:)
185
- # theme_hash = Models::ColorTheme.send(:replace, color_theme: default,
186
- # replace_color: :cyan, with_color: base_color).tap do |hash|
187
- # hash[:description] = description
188
- # end
189
- # new(theme_name: theme_name, theme_hash: theme_hash)
190
- # end
191
-
192
184
  private
193
185
 
194
186
  def default_theme_color_keys
195
187
  DEFAULT_THEME_COLORS.keys
196
188
  end
197
-
198
- def replace(color_theme:, replace_color:, with_color:)
199
- colors_theme_hash = color_theme.to_theme_colors_h.tap do |hash|
200
- hash.each_key do |key|
201
- hash[key] = replace_color(theme_color: hash[key],
202
- replace_color: replace_color, with_color: with_color)
203
- end
204
- end
205
- DEFAULT_THEME.merge(colors_theme_hash)
206
- end
207
-
208
- def replace_color(theme_color:, replace_color:, with_color:)
209
- %i[color background].each do |color_type|
210
- color = theme_color[color_type].to_s.sub(replace_color.to_s, with_color.to_s)
211
- theme_color[color_type] = color.sub('light_light_', 'light_').to_sym
212
- end
213
- theme_color
214
- end
215
-
216
- # If the color theme is deleted (deleted_theme_name) and the current
217
- # theme_name in the configuration is the same as the deleted theme,
218
- # we need to reset the configuration theme to the default theme.
219
- def reset_default_configuration_color_theme_if!(deleted_theme_name:)
220
- config = configuration
221
- return if config.theme_name == self::DEFAULT_THEME_NAME
222
- return unless config.theme_name == deleted_theme_name
223
- return unless config.exist?
224
-
225
- config.theme_name = self::DEFAULT_THEME_NAME
226
- config.write!
227
- end
228
-
229
- def themes_path_for(theme_name:)
230
- Support::Fileable.themes_path(theme_name: theme_name)
231
- end
232
189
  end
233
190
 
234
191
  def to_h
@@ -239,14 +196,6 @@ module Dsu
239
196
  end
240
197
  end
241
198
 
242
- def to_theme_colors_h
243
- {}.tap do |hash|
244
- DEFAULT_THEME_COLORS.each_key do |key|
245
- hash[key] = public_send(key)
246
- end
247
- end
248
- end
249
-
250
199
  def ==(other)
251
200
  return false unless other.is_a?(self.class)
252
201
  return false unless other.theme_name == theme_name
@@ -42,7 +42,9 @@ module Dsu
42
42
  # The currently selected color theme. Should be equal to
43
43
  # Models::ColorTheme::DEFAULT_THEME_NAME or the name of a custom
44
44
  # theme (with the same file name) that resides in the themes_folder.
45
- theme_name: 'default'
45
+ theme_name: 'default',
46
+ # The default project to use.
47
+ default_project: 'default'
46
48
  }.freeze
47
49
 
48
50
  validates_with Validators::VersionValidator
@@ -53,13 +55,16 @@ module Dsu
53
55
  validates :include_all, inclusion: { in: [true, false], message: 'must be true or false' }
54
56
  validates :theme_name, presence: true
55
57
  validate :validate_theme_file
58
+ validates :default_project, presence: true
59
+ validate :validate_default_project
56
60
 
57
61
  attr_accessor :version,
58
62
  :editor,
59
63
  :entries_display_order,
60
64
  :carry_over_entries_to_today,
61
65
  :include_all,
62
- :theme_name
66
+ :theme_name,
67
+ :default_project
63
68
 
64
69
  attr_reader :options
65
70
 
@@ -114,7 +119,8 @@ module Dsu
114
119
  entries_display_order: entries_display_order,
115
120
  carry_over_entries_to_today: carry_over_entries_to_today,
116
121
  include_all: include_all,
117
- theme_name: theme_name
122
+ theme_name: theme_name,
123
+ default_project: default_project
118
124
  }
119
125
  end
120
126
 
@@ -150,6 +156,7 @@ module Dsu
150
156
  DEFAULT_CONFIGURATION[:carry_over_entries_to_today])
151
157
  @include_all = config_hash.fetch(:include_all, DEFAULT_CONFIGURATION[:include_all])
152
158
  @theme_name = config_hash.fetch(:theme_name, DEFAULT_CONFIGURATION[:theme_name])
159
+ @default_project = config_hash.fetch(:default_project, DEFAULT_CONFIGURATION[:default_project])
153
160
  end
154
161
 
155
162
  def validate_theme_file
@@ -159,6 +166,14 @@ module Dsu
159
166
  i18n_key = 'configuration.errors.theme_file_missing'
160
167
  errors.add(:base, I18n.t(i18n_key, theme_path: theme_path))
161
168
  end
169
+
170
+ def validate_default_project
171
+ default_project_folder = File.join(projects_folder, default_project.presence || '{{blank}}')
172
+ return if Dir.exist?(default_project_folder)
173
+
174
+ i18n_key = 'configuration.errors.project_path_missing'
175
+ errors.add(:base, I18n.t(i18n_key, project_folder: default_project_folder))
176
+ end
162
177
  end
163
178
  end
164
179
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_model'
4
3
  require_relative '../crud/json_file'
5
4
  require_relative '../migration/version'
6
5
  require_relative '../services/entry_group/editor_service'
@@ -177,12 +176,6 @@ module Dsu
177
176
  Services::EntryGroup::HydratorService.new(entry_group_hash: entry_group_hash).call
178
177
  end
179
178
 
180
- def find_or_create(time:)
181
- find_or_initialize(time: time).tap do |entry_group|
182
- entry_group.write! unless entry_group.exist?
183
- end
184
- end
185
-
186
179
  def find_or_initialize(time:)
187
180
  file_path = entries_path_for(time: time)
188
181
  read(file_path: file_path) do |entry_group_hash|
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_model'
4
3
  require_relative '../crud/json_file'
5
4
  require_relative '../services/migration_version/hydrator_service'
6
5
  require_relative '../validators/version_validator'
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ require_relative '../crud/json_file'
6
+ require_relative '../migration/version'
7
+ require_relative '../models/configuration'
8
+ require_relative '../services/project/hydrator_service'
9
+ require_relative '../services/project/rename_service'
10
+ require_relative '../support/descriptable'
11
+ require_relative '../support/fileable'
12
+ require_relative '../support/project_file_system'
13
+ require_relative '../validators/description_validator'
14
+ require_relative '../validators/project_name_validator'
15
+ require_relative '../validators/version_validator'
16
+
17
+ module Dsu
18
+ module Models
19
+ # This class represents a project. A project is a collection of entry groups.
20
+ class Project
21
+ include ActiveModel::Model
22
+ include Support::Descriptable
23
+ include Support::Fileable
24
+ include Support::ProjectFileSystem
25
+
26
+ VERSION = Migration::VERSION
27
+ MIN_PROJECT_NAME_LENGTH = 2
28
+ MAX_PROJECT_NAME_LENGTH = 24
29
+ MIN_DESCRIPTION_LENGTH = 2
30
+ MAX_DESCRIPTION_LENGTH = 32
31
+
32
+ attr_reader :project_name, :current_project_file, :description, :version, :options
33
+
34
+ validates_with Validators::DescriptionValidator
35
+ validates_with Validators::ProjectNameValidator
36
+ validates_with Validators::VersionValidator
37
+
38
+ def initialize(project_name:, description: nil, version: nil, options: {})
39
+ raise ArgumentError, 'project_name is blank' if project_name.blank?
40
+ raise ArgumentError, 'version is the wrong object type' unless version.is_a?(Integer) || version.nil?
41
+
42
+ self.project_name = project_name
43
+ self.description = description
44
+ self.version = version || VERSION
45
+ self.options = options || {}
46
+ end
47
+
48
+ # Override == and hash so that we can compare Entry Group objects.
49
+ def ==(other)
50
+ other.is_a?(Project) &&
51
+ project_name == other.project_name &&
52
+ description == other.description &&
53
+ version == other.version
54
+ end
55
+ alias eql? ==
56
+
57
+ def can_delete?
58
+ self.class.can_delete?(project_name: project_name)
59
+ end
60
+
61
+ def create
62
+ self.class.create(project_name: project_name, description: description)
63
+ end
64
+ alias save create
65
+
66
+ def create!
67
+ self.class.create!(project_name: project_name, description: description)
68
+ end
69
+ alias save! create!
70
+
71
+ def current_project?
72
+ self.class.current_project?(project_name: project_name)
73
+ end
74
+
75
+ def default!
76
+ return if default_project?
77
+
78
+ self.class.default!(project: self)
79
+ end
80
+
81
+ def default_project?
82
+ self.class.default_project?(project_name: project_name)
83
+ end
84
+
85
+ def delete
86
+ self.class.delete(project_name: project_name)
87
+ end
88
+
89
+ def delete!
90
+ self.class.delete!(project_name: project_name)
91
+ end
92
+
93
+ def hash
94
+ [project_name, description, version].map(&:hash).hash
95
+ end
96
+
97
+ def project_file
98
+ self.class.project_file(project_name: project_name)
99
+ end
100
+
101
+ def project_folder
102
+ self.class.project_folder(project_name: project_name)
103
+ end
104
+
105
+ def rename!(new_project_name:, new_project_description: nil)
106
+ self.class.rename!(project_name: project_name,
107
+ new_project_name: new_project_name, new_project_description: new_project_description, options: options)
108
+ end
109
+
110
+ def to_h
111
+ {
112
+ version: version,
113
+ project_name: project_name,
114
+ description: description
115
+ }
116
+ end
117
+
118
+ # def update
119
+ # self.class.update(project_name: project_name, description: description, version: version, options: options)
120
+ # end
121
+
122
+ # def update!
123
+ # self.class.update!(project_name: project_name, description: description, version: version, options: options)
124
+ # end
125
+
126
+ def use!
127
+ return if current_project?
128
+
129
+ self.class.use!(project: self)
130
+ end
131
+
132
+ class << self
133
+ delegate :project_file_for, :project_folder_for, to: Support::Fileable
134
+
135
+ def all
136
+ project_metadata.map do |metadata|
137
+ find(project_name: metadata[:project_name])
138
+ end
139
+ end
140
+
141
+ def can_delete?(project_name:)
142
+ exist?(project_name: project_name) &&
143
+ # Cannot delete the last project.
144
+ count > 1 &&
145
+ # Do not allow the project to be deleted if it
146
+ # is currently the default project.
147
+ # The user needs to change to another default
148
+ # project before they can delete this project.
149
+ !default_project?(project_name: project_name)
150
+ end
151
+
152
+ def count
153
+ project_metadata.count
154
+ end
155
+
156
+ def create(project_name:, description: nil, options: {})
157
+ Models::Project.new(project_name: project_name, description: description, options: options).tap do |project|
158
+ project.validate!
159
+ initialize_project(project_name: project_name)
160
+ Crud::JsonFile.write!(file_data: project.to_h,
161
+ file_path: project_file_for(project_name: project_name))
162
+ end
163
+ end
164
+ alias update create
165
+
166
+ def create!(project_name:, description: nil, options: {})
167
+ if exist?(project_name: project_name)
168
+ raise I18n.t('models.project.errors.already_exists', project_name: project_name)
169
+ end
170
+
171
+ create(project_name: project_name, description: description, options: options)
172
+ end
173
+ alias update! create!
174
+
175
+ def current_project
176
+ find(project_name: current_project_name)
177
+ end
178
+
179
+ def current_project?(project_name:)
180
+ current_project_name == project_name
181
+ end
182
+
183
+ def default!(project:)
184
+ project.validate!
185
+
186
+ Models::Configuration.new.tap do |configuration|
187
+ configuration.default_project = project.project_name
188
+ configuration.save!
189
+ end
190
+ end
191
+
192
+ def default_project
193
+ find(project_name: default_project_name)
194
+ end
195
+
196
+ def default_project?(project_name:)
197
+ project_name == default_project_name
198
+ end
199
+
200
+ def delete(project_name:)
201
+ return false unless can_delete?(project_name: project_name)
202
+
203
+ project_folder = project_folder_for(project_name: project_name)
204
+ FileUtils.rm_rf(project_folder)
205
+
206
+ true
207
+ end
208
+
209
+ def delete!(project_name:)
210
+ unless exist?(project_name: project_name)
211
+ raise I18n.t('models.project.errors.does_not_exist', project_name: project_name)
212
+ end
213
+
214
+ raise I18n.t('models.project.errors.delete_only_project', project_name: project_name) unless count > 1
215
+
216
+ if default_project?(project_name: project_name)
217
+ raise I18n.t('models.project.errors.delete_default_project', project_name: project_name)
218
+ end
219
+
220
+ delete(project_name: project_name)
221
+ end
222
+
223
+ def find(project_name:)
224
+ unless project_folder_exist?(project_name: project_name)
225
+ raise I18n.t('models.project.errors.does_not_exist', project_name: project_name)
226
+ end
227
+
228
+ project_file = project_file_for(project_name: project_name)
229
+
230
+ unless project_file_exist?(project_name: project_name)
231
+ raise I18n.t('models.project.errors.project_file_not_exist', project_file: project_file)
232
+ end
233
+
234
+ project_hash = Crud::JsonFile.read!(file_path: project_file)
235
+ Services::Project::HydratorService.new(project_hash: project_hash).call
236
+ end
237
+
238
+ # project_number is 1 based.
239
+ def find_by_number(project_number:)
240
+ project = project_metadata.find do |metadata|
241
+ metadata[:project_number] == project_number.to_i
242
+ end
243
+ return unless project
244
+
245
+ find(project_name: project[:project_name])
246
+ end
247
+
248
+ # def find_or_create(project_name:)
249
+ # find_or_initialize(project_name: project_name).tap do |project|
250
+ # project.save! unless project.persisted?
251
+ # end
252
+ # end
253
+
254
+ def find_or_initialize(project_name:)
255
+ return Models::Project.new(project_name: project_name) unless project_file_exist?(project_name: project_name)
256
+
257
+ project_file = project_file_for(project_name: project_name)
258
+ project_hash = Crud::JsonFile.read!(file_path: project_file)
259
+ Services::Project::HydratorService.new(project_hash: project_hash).call
260
+ end
261
+
262
+ def rename!(project_name:, new_project_name:, new_project_description: nil, options: {})
263
+ Services::Project::RenameService.new(from_project_name: project_name,
264
+ to_project_name: new_project_name, to_project_description: new_project_description, options: options).call
265
+ end
266
+
267
+ def use!(project:)
268
+ project.validate!
269
+
270
+ current_project_hash = { version: project.version, project_name: project.project_name }
271
+ Crud::JsonFile.write!(file_data: current_project_hash, file_path: current_project_file)
272
+ end
273
+ end
274
+
275
+ private
276
+
277
+ attr_writer :current_project_file, :options, :version
278
+
279
+ def description=(value)
280
+ @description = if value.blank?
281
+ "#{project_name} project"
282
+ else
283
+ value
284
+ end
285
+ end
286
+
287
+ def project_name=(value)
288
+ @project_name = begin
289
+ @current_project_file = project_folder_for(project_name: value)
290
+ value
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
@@ -1,26 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'delegate'
4
- require_relative '../models/color_theme'
5
- require_relative '../support/color_themable'
6
-
7
3
  module Dsu
8
4
  module Presenters
9
5
  class BasePresenterEx
10
- include Support::ColorThemable
11
-
12
6
  def initialize(options: {})
13
7
  @options = options&.dup || {}
14
- @color_theme = Models::ColorTheme.find(theme_name: theme_name)
15
8
  end
16
9
 
17
10
  private
18
11
 
19
- attr_reader :color_theme, :options
20
-
21
- def theme_name
22
- @theme_name ||= options.fetch(:theme_name, Models::Configuration.new.theme_name)
23
- end
12
+ attr_accessor :options
24
13
  end
25
14
  end
26
15
  end