dsu 2.4.4 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -0
- data/CHANGELOG.md +42 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/Rakefile +6 -0
- data/current_project.bak +4 -0
- data/lib/dsu/cli.rb +24 -6
- data/lib/dsu/crud/json_file.rb +3 -0
- data/lib/dsu/migration/version.rb +1 -1
- data/lib/dsu/models/color_theme.rb +7 -58
- data/lib/dsu/models/configuration.rb +18 -3
- data/lib/dsu/models/entry_group.rb +0 -7
- data/lib/dsu/models/migration_version.rb +0 -1
- data/lib/dsu/models/project.rb +295 -0
- data/lib/dsu/presenters/base_presenter_ex.rb +1 -12
- data/lib/dsu/presenters/export/all_presenter.rb +14 -19
- data/lib/dsu/presenters/export/dates_presenter.rb +17 -20
- data/lib/dsu/presenters/import/all_presenter.rb +20 -25
- data/lib/dsu/presenters/import/dates_presenter.rb +25 -27
- data/lib/dsu/presenters/import/import_entry.rb +22 -0
- data/lib/dsu/presenters/import/import_file.rb +9 -1
- data/lib/dsu/presenters/project/create_presenter.rb +44 -0
- data/lib/dsu/presenters/project/delete_by_number_presenter.rb +54 -0
- data/lib/dsu/presenters/project/delete_presenter.rb +53 -0
- data/lib/dsu/presenters/project/list_presenter.rb +24 -0
- data/lib/dsu/presenters/project/rename_by_number_presenter.rb +63 -0
- data/lib/dsu/presenters/project/rename_presenter.rb +57 -0
- data/lib/dsu/presenters/project/use_by_number_presenter.rb +53 -0
- data/lib/dsu/presenters/project/use_presenter.rb +52 -0
- data/lib/dsu/services/entry_group/exporter_service.rb +22 -5
- data/lib/dsu/services/entry_group/importer_service.rb +41 -8
- data/lib/dsu/services/project/hydrator_service.rb +40 -0
- data/lib/dsu/services/project/rename_service.rb +70 -0
- data/lib/dsu/subcommands/export.rb +4 -2
- data/lib/dsu/subcommands/import.rb +7 -3
- data/lib/dsu/subcommands/project.rb +149 -0
- data/lib/dsu/support/ask.rb +10 -3
- data/lib/dsu/support/color_themable.rb +1 -1
- data/lib/dsu/support/command_hookable.rb +7 -2
- data/lib/dsu/support/descriptable.rb +5 -21
- data/lib/dsu/support/fileable.rb +39 -1
- data/lib/dsu/support/project_file_system.rb +121 -0
- data/lib/dsu/support/short_string.rb +24 -0
- data/lib/dsu/support/time_comparable.rb +2 -0
- data/lib/dsu/support/transform_project_name.rb +24 -0
- data/lib/dsu/validators/project_name_validator.rb +58 -0
- data/lib/dsu/version.rb +1 -1
- data/lib/dsu/views/base_list_view.rb +41 -0
- data/lib/dsu/views/export.rb +60 -6
- data/lib/dsu/views/import.rb +83 -7
- data/lib/dsu/views/import_dates.rb +17 -0
- data/lib/dsu/views/project/create.rb +87 -0
- data/lib/dsu/views/project/delete.rb +96 -0
- data/lib/dsu/views/project/delete_by_number.rb +19 -0
- data/lib/dsu/views/project/list.rb +115 -0
- data/lib/dsu/views/project/rename.rb +98 -0
- data/lib/dsu/views/project/rename_by_number.rb +21 -0
- data/lib/dsu/views/project/use.rb +97 -0
- data/lib/dsu/views/project/use_by_number.rb +19 -0
- data/lib/dsu.rb +2 -10
- data/lib/locales/en/active_record.yml +9 -0
- data/lib/locales/en/commands.yml +9 -3
- data/lib/locales/en/miscellaneous.yml +4 -0
- data/lib/locales/en/services.yml +4 -0
- data/lib/locales/en/subcommands.yml +247 -15
- data/project.bak +0 -0
- metadata +34 -9
- data/lib/dsu/presenters/export/messages.rb +0 -32
- data/lib/dsu/presenters/export/nothing_to_export.rb +0 -13
- data/lib/dsu/presenters/export/service_callable.rb +0 -20
- data/lib/dsu/presenters/import/messages.rb +0 -42
- data/lib/dsu/presenters/import/service_callable.rb +0 -21
@@ -23,7 +23,8 @@ module Dsu
|
|
23
23
|
long_desc I18n.t('subcommands.export.all.long_desc')
|
24
24
|
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
25
25
|
def all
|
26
|
-
|
26
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
27
|
+
Views::Export.new(presenter: all_presenter(options: options), options: options).render
|
27
28
|
end
|
28
29
|
|
29
30
|
desc I18n.t('subcommands.export.dates.desc'), I18n.t('subcommands.export.dates.usage')
|
@@ -41,7 +42,8 @@ module Dsu
|
|
41
42
|
return
|
42
43
|
end
|
43
44
|
|
44
|
-
Views::Export.new(presenter:
|
45
|
+
Views::Export.new(presenter:
|
46
|
+
dates_presenter_for(from: times.min, to: times.max, options: options), options: options).render
|
45
47
|
rescue ArgumentError => e
|
46
48
|
Views::Shared::Error.new(messages: e.message).render
|
47
49
|
end
|
@@ -6,6 +6,7 @@ require_relative '../support/command_options/dsu_times'
|
|
6
6
|
require_relative '../support/command_options/time_mnemonic'
|
7
7
|
require_relative '../support/time_formatable'
|
8
8
|
require_relative '../views/import'
|
9
|
+
require_relative '../views/import_dates'
|
9
10
|
require_relative '../views/shared/error'
|
10
11
|
require_relative 'base_subcommand'
|
11
12
|
|
@@ -23,10 +24,12 @@ module Dsu
|
|
23
24
|
long_desc I18n.t('subcommands.import.all.long_desc')
|
24
25
|
option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
|
25
26
|
option :merge, type: :boolean, default: true, aliases: '-m'
|
27
|
+
option :override, type: :boolean, default: false, aliases: '-o'
|
26
28
|
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
27
29
|
def all
|
30
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
28
31
|
Views::Import.new(presenter: all_presenter(import_file_path: options[:import_file],
|
29
|
-
options: options)).render
|
32
|
+
options: options), options: options).render
|
30
33
|
end
|
31
34
|
|
32
35
|
desc I18n.t('subcommands.import.dates.desc'), I18n.t('subcommands.import.dates.usage')
|
@@ -37,6 +40,7 @@ module Dsu
|
|
37
40
|
option :to, type: :string, required: true, aliases: '-t', banner: 'DATE|MNEMONIC'
|
38
41
|
option :import_file, type: :string, required: true, aliases: '-i', banner: 'IMPORT_CVS_FILE'
|
39
42
|
option :merge, type: :boolean, default: true, aliases: '-m'
|
43
|
+
option :override, type: :boolean, default: false, aliases: '-o'
|
40
44
|
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
41
45
|
def dates
|
42
46
|
options = configuration.to_h.merge(self.options).with_indifferent_access
|
@@ -46,10 +50,10 @@ module Dsu
|
|
46
50
|
return
|
47
51
|
end
|
48
52
|
|
49
|
-
Views::
|
53
|
+
Views::ImportDates.new(presenter: dates_presenter_for(from: times.min,
|
50
54
|
to: times.max,
|
51
55
|
import_file_path: options[:import_file],
|
52
|
-
options: options)).render
|
56
|
+
options: options), options: options).render
|
53
57
|
rescue ArgumentError => e
|
54
58
|
Views::Shared::Error.new(messages: e.message).render
|
55
59
|
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../presenters/project/create_presenter'
|
4
|
+
require_relative '../presenters/project/delete_presenter'
|
5
|
+
require_relative '../presenters/project/list_presenter'
|
6
|
+
require_relative '../presenters/project/use_by_number_presenter'
|
7
|
+
require_relative '../presenters/project/use_presenter'
|
8
|
+
require_relative '../views/project/create'
|
9
|
+
require_relative '../views/project/use'
|
10
|
+
require_relative '../views/project/use_by_number'
|
11
|
+
require_relative '../views/shared/error'
|
12
|
+
require_relative 'base_subcommand'
|
13
|
+
|
14
|
+
module Dsu
|
15
|
+
module Subcommands
|
16
|
+
class Project < BaseSubcommand
|
17
|
+
# TODO: I18n.
|
18
|
+
map %w[c] => :create
|
19
|
+
map %w[d] => :delete
|
20
|
+
map %w[l] => :list
|
21
|
+
map %w[r] => :rename
|
22
|
+
map %w[u] => :use
|
23
|
+
|
24
|
+
desc I18n.t('subcommands.project.create.desc'), I18n.t('subcommands.project.create.usage')
|
25
|
+
long_desc I18n.t('subcommands.project.create.long_desc')
|
26
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
27
|
+
def create(project_name = nil, description = nil)
|
28
|
+
project_name = project_name.to_s.strip
|
29
|
+
description = description.to_s.strip
|
30
|
+
if project_name.blank?
|
31
|
+
return Views::Shared::Error.new(
|
32
|
+
messages: I18n.t('subcommands.project.messages.project_name_blank')
|
33
|
+
).render
|
34
|
+
end
|
35
|
+
|
36
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
37
|
+
presenter = Presenters::Project::CreatePresenter.new(project_name: project_name,
|
38
|
+
description: description, options: options)
|
39
|
+
Views::Project::Create.new(presenter: presenter, options: options).render
|
40
|
+
end
|
41
|
+
|
42
|
+
desc I18n.t('subcommands.project.delete.desc'), I18n.t('subcommands.project.delete.usage')
|
43
|
+
long_desc I18n.t('subcommands.project.delete.long_desc')
|
44
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
45
|
+
def delete(project_name_or_number = nil)
|
46
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
47
|
+
presenter = delete_presenter_for(project_name_or_number, options: options)
|
48
|
+
delete_view_for(project_name_or_number, presenter: presenter, options: options).render
|
49
|
+
end
|
50
|
+
|
51
|
+
desc I18n.t('subcommands.project.list.desc'), I18n.t('subcommands.project.list.usage')
|
52
|
+
long_desc I18n.t('subcommands.project.list.long_desc')
|
53
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
54
|
+
def list
|
55
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
56
|
+
presenter = Presenters::Project::ListPresenter.new(options: options)
|
57
|
+
Views::Project::List.new(presenter: presenter, options: options).render
|
58
|
+
end
|
59
|
+
|
60
|
+
desc I18n.t('subcommands.project.rename.desc'), I18n.t('subcommands.project.rename.usage')
|
61
|
+
long_desc I18n.t('subcommands.project.rename.long_desc')
|
62
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
63
|
+
def rename(project_name_or_number = nil, new_project_name = nil, new_project_description = nil)
|
64
|
+
project_name_or_number = project_name_or_number.to_s.strip
|
65
|
+
new_project_name = new_project_name&.to_s&.strip
|
66
|
+
new_project_description = new_project_description&.to_s&.strip
|
67
|
+
|
68
|
+
if new_project_name.blank?
|
69
|
+
return Views::Shared::Error.new(
|
70
|
+
messages: I18n.t('subcommands.project.messages.new_project_name_blank')
|
71
|
+
).render
|
72
|
+
end
|
73
|
+
|
74
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
75
|
+
presenter = rename_presenter_for(project_name_or_number, new_project_name: new_project_name,
|
76
|
+
new_project_description: new_project_description, options: options)
|
77
|
+
rename_view_for(project_name_or_number, presenter: presenter, options: options).render
|
78
|
+
end
|
79
|
+
|
80
|
+
desc I18n.t('subcommands.project.use.desc'), I18n.t('subcommands.project.use.usage')
|
81
|
+
long_desc I18n.t('subcommands.project.use.long_desc')
|
82
|
+
option :prompts, type: :hash, default: {}, hide: true, aliases: '-p'
|
83
|
+
def use(project_name_or_number = nil)
|
84
|
+
options = configuration.to_h.merge(self.options).with_indifferent_access
|
85
|
+
presenter = use_presenter_for(project_name_or_number, options: options)
|
86
|
+
use_view_for(project_name_or_number, presenter: presenter, options: options).render
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def delete_view_for(project_name, presenter:, options:)
|
92
|
+
if project_number?(project_name)
|
93
|
+
Views::Project::DeleteByNumber.new(presenter: presenter, options: options)
|
94
|
+
else
|
95
|
+
Views::Project::Delete.new(presenter: presenter, options: options)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete_presenter_for(project_name, options:)
|
100
|
+
if project_number?(project_name)
|
101
|
+
Presenters::Project::DeleteByNumberPresenter.new(project_number: project_name.to_i, options: options)
|
102
|
+
else
|
103
|
+
project_name = Models::Project.default_project_name if project_name.blank?
|
104
|
+
Presenters::Project::DeletePresenter.new(project_name: project_name, options: options)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def rename_view_for(project_name, presenter:, options:)
|
109
|
+
if project_number?(project_name)
|
110
|
+
Views::Project::RenameByNumber.new(presenter: presenter, options: options)
|
111
|
+
else
|
112
|
+
Views::Project::Rename.new(presenter: presenter, options: options)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def rename_presenter_for(project_name, new_project_name:, new_project_description:, options:)
|
117
|
+
if project_number?(project_name)
|
118
|
+
Presenters::Project::RenameByNumberPresenter.new(project_number: project_name.to_i,
|
119
|
+
new_project_name: new_project_name, new_project_description: new_project_description, options: options)
|
120
|
+
else
|
121
|
+
project_name = Models::Project.default_project_name if project_name.blank?
|
122
|
+
Presenters::Project::RenamePresenter.new(project_name: project_name,
|
123
|
+
new_project_name: new_project_name, new_project_description: new_project_description, options: options)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def use_view_for(project_name, presenter:, options:)
|
128
|
+
if project_number?(project_name)
|
129
|
+
Views::Project::UseByNumber.new(presenter: presenter, options: options)
|
130
|
+
else
|
131
|
+
Views::Project::Use.new(presenter: presenter, options: options)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def use_presenter_for(project_name, options:)
|
136
|
+
if project_number?(project_name)
|
137
|
+
Presenters::Project::UseByNumberPresenter.new(project_number: project_name.to_i, options: options)
|
138
|
+
else
|
139
|
+
project_name = Models::Project.default_project_name if project_name.blank?
|
140
|
+
Presenters::Project::UsePresenter.new(project_name: project_name, options: options)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def project_number?(project_name)
|
145
|
+
/^[+-]?\d+(\.\d+)?$/.match?(project_name.to_s)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
data/lib/dsu/support/ask.rb
CHANGED
@@ -1,13 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'io/console'
|
3
4
|
require 'thor'
|
4
5
|
|
5
6
|
module Dsu
|
6
7
|
module Support
|
7
8
|
module Ask
|
8
|
-
def
|
9
|
-
|
10
|
-
|
9
|
+
def ask_while(prompt, options: {}) # rubocop:disable Lint/UnusedMethodArgument
|
10
|
+
loop do
|
11
|
+
print prompt
|
12
|
+
char = $stdin.getch
|
13
|
+
puts char
|
14
|
+
return char if yield(char)
|
15
|
+
|
16
|
+
char
|
17
|
+
end
|
11
18
|
end
|
12
19
|
|
13
20
|
def yes?(prompt, options: {})
|
@@ -8,7 +8,7 @@ module Dsu
|
|
8
8
|
def prompt_with_options(prompt:, options:)
|
9
9
|
# HACK: This module needs to be refactored to be more generic.
|
10
10
|
target_color_theme = defined?(color_theme) ? color_theme : self
|
11
|
-
options = "[#{options.join('
|
11
|
+
options = "[#{options.join(',')}]"
|
12
12
|
"#{apply_theme(prompt, theme_color: target_color_theme.prompt)} " \
|
13
13
|
"#{apply_theme(options, theme_color: target_color_theme.prompt_options)}" \
|
14
14
|
"#{apply_theme('>', theme_color: target_color_theme.prompt)}"
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../env'
|
4
4
|
require_relative '../models/color_theme'
|
5
|
+
require_relative '../models/project'
|
5
6
|
require_relative '../services/stderr_redirector_service'
|
6
7
|
require_relative '../views/shared/error'
|
7
8
|
require_relative 'color_themable'
|
@@ -34,14 +35,18 @@ module Dsu
|
|
34
35
|
end
|
35
36
|
|
36
37
|
def display_dsu_footer
|
37
|
-
puts apply_theme('_' *
|
38
|
+
puts apply_theme('_' * 50, theme_color: color_theme.dsu_footer)
|
38
39
|
# TODO: I18n.
|
39
|
-
puts apply_theme("dsu |
|
40
|
+
puts apply_theme("dsu v#{Dsu::VERSION} | Project: #{project} | Theme: #{color_theme.theme_name}",
|
40
41
|
theme_color: color_theme.dsu_footer)
|
41
42
|
end
|
42
43
|
|
43
44
|
private
|
44
45
|
|
46
|
+
def project
|
47
|
+
Models::Project.current_project_name
|
48
|
+
end
|
49
|
+
|
45
50
|
def suspend_header?(args, _options)
|
46
51
|
return false unless args.count > 1
|
47
52
|
|
@@ -1,10 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'short_string'
|
4
|
+
|
3
5
|
module Dsu
|
4
6
|
module Support
|
5
7
|
module Descriptable
|
6
|
-
DESCRIPTION_MAX_COUNT = 25
|
7
|
-
|
8
8
|
class << self
|
9
9
|
def included(base)
|
10
10
|
base.extend(ClassMethods)
|
@@ -18,26 +18,10 @@ module Dsu
|
|
18
18
|
end
|
19
19
|
|
20
20
|
module ClassMethods
|
21
|
-
|
22
|
-
return elipsis unless string.is_a?(String)
|
23
|
-
|
24
|
-
elipsis_length = elipsis.length
|
25
|
-
count = elipsis_length if count.nil? || count < elipsis_length
|
26
|
-
|
27
|
-
return string if string.length <= count
|
28
|
-
|
29
|
-
tokens = string.split
|
30
|
-
string = ''
|
31
|
-
|
32
|
-
return "#{tokens.first[0...(count - elipsis_length)]}#{elipsis}" if tokens.count == 1
|
33
|
-
|
34
|
-
tokens.each do |token|
|
35
|
-
break if string.length + token.length + elipsis_length > count
|
36
|
-
|
37
|
-
string = "#{string} #{token}"
|
38
|
-
end
|
21
|
+
include ShortString
|
39
22
|
|
40
|
-
|
23
|
+
def short_description(string:, count: ShortString::SHORT_STRING_MAX_COUNT, elipsis: '...')
|
24
|
+
short_string(string: string, count: count, elipsis: elipsis)
|
41
25
|
end
|
42
26
|
end
|
43
27
|
end
|
data/lib/dsu/support/fileable.rb
CHANGED
@@ -26,7 +26,8 @@ module Dsu
|
|
26
26
|
# Entries
|
27
27
|
|
28
28
|
def entries_folder
|
29
|
-
|
29
|
+
project_folder = project_folder_for(project_name: Models::Project.current_project_name)
|
30
|
+
File.join(project_folder, 'entries')
|
30
31
|
end
|
31
32
|
|
32
33
|
def entries_file_name(time:, file_name_format: nil)
|
@@ -88,6 +89,43 @@ module Dsu
|
|
88
89
|
File.join(gem_dir, 'lib/seed_data')
|
89
90
|
end
|
90
91
|
|
92
|
+
# Projects
|
93
|
+
|
94
|
+
# Returns the folder where all the projects are stored.
|
95
|
+
def projects_folder
|
96
|
+
File.join(dsu_folder, 'projects')
|
97
|
+
end
|
98
|
+
|
99
|
+
# Current project
|
100
|
+
|
101
|
+
# Contains the name of the file that contains the current
|
102
|
+
# dsu project currently being used.
|
103
|
+
def current_project_file_name
|
104
|
+
'current_project.json'
|
105
|
+
end
|
106
|
+
|
107
|
+
# The complete path to the current project file.
|
108
|
+
def current_project_file
|
109
|
+
File.join(dsu_folder, current_project_file_name)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Project helpers
|
113
|
+
|
114
|
+
# Returns the path of the project with the given name.
|
115
|
+
def project_folder_for(project_name:)
|
116
|
+
raise I18n.t('errors.project_name_invalid', project_name: '{{blank}}') if project_name.blank?
|
117
|
+
|
118
|
+
File.join(projects_folder, project_name)
|
119
|
+
end
|
120
|
+
alias project_folder project_folder_for
|
121
|
+
|
122
|
+
def project_file_for(project_name:)
|
123
|
+
project_folder = project_folder_for(project_name: project_name)
|
124
|
+
|
125
|
+
File.join(project_folder, 'project.json')
|
126
|
+
end
|
127
|
+
alias project_file project_file_for
|
128
|
+
|
91
129
|
extend self # rubocop:disable Style/ModuleFunction
|
92
130
|
end
|
93
131
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
require 'pathname'
|
5
|
+
require_relative '../crud/json_file'
|
6
|
+
require_relative '../migration/version'
|
7
|
+
require_relative '../models/configuration'
|
8
|
+
require_relative 'fileable'
|
9
|
+
|
10
|
+
module Dsu
|
11
|
+
module Support
|
12
|
+
module ProjectFileSystem
|
13
|
+
class << self
|
14
|
+
def included(base)
|
15
|
+
base.extend(ClassMethods)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def exist?
|
20
|
+
self.class.project_file_exist?(project_name: project_name)
|
21
|
+
end
|
22
|
+
alias persisted? exist?
|
23
|
+
|
24
|
+
def project_initialized?
|
25
|
+
self.class.project_initialized?(project_name: project_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def project_number
|
29
|
+
self.class.project_number_for(project_name: project_name)
|
30
|
+
end
|
31
|
+
|
32
|
+
module ClassMethods
|
33
|
+
include Fileable
|
34
|
+
|
35
|
+
# Returns the currently selected (used) project name
|
36
|
+
# from dsu/current_project.json
|
37
|
+
def current_project_name
|
38
|
+
Crud::JsonFile.read!(file_path: current_project_file).fetch(:project_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
def default_project_name
|
42
|
+
Models::Configuration.new.default_project
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize_project(project_name:)
|
46
|
+
return if project_initialized?(project_name: project_name)
|
47
|
+
|
48
|
+
# TODO: Don't know if I like this here.
|
49
|
+
unless current_project_file_exist?
|
50
|
+
file_data = {
|
51
|
+
version: Dsu::Migration::VERSION,
|
52
|
+
project_name: default_project_name
|
53
|
+
}
|
54
|
+
Crud::JsonFile.write!(file_data: file_data, file_path: current_project_file)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Creates dsu/projects/<project_name>
|
58
|
+
FileUtils.mkdir_p(project_folder_for(project_name: project_name))
|
59
|
+
end
|
60
|
+
|
61
|
+
def project_initialized?(project_name:)
|
62
|
+
# Checking these files, checks all the containing folders also
|
63
|
+
current_project_file_exist? &&
|
64
|
+
project_folder_exist?(project_name: project_name)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Does dsu/projects/<project_name>/project.json file exist?
|
68
|
+
def project_file_exist?(project_name:)
|
69
|
+
project_file_path = project_file_for(project_name: project_name)
|
70
|
+
File.exist?(project_file_path)
|
71
|
+
end
|
72
|
+
alias exist? project_file_exist?
|
73
|
+
alias persisted? project_file_exist?
|
74
|
+
|
75
|
+
# Does dsu/current_project.json file exist?
|
76
|
+
def current_project_file_exist?
|
77
|
+
File.exist?(current_project_file)
|
78
|
+
end
|
79
|
+
alias current_project_file_persisted? current_project_file_exist?
|
80
|
+
|
81
|
+
# Does dsu/projects folder exist?
|
82
|
+
def projects_folder_exist?
|
83
|
+
Dir.exist?(projects_folder)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Does dsu/projects/<project_name> folder exist?
|
87
|
+
def project_folder_exist?(project_name:)
|
88
|
+
Dir.exist?(project_folder_for(project_name: project_name))
|
89
|
+
end
|
90
|
+
|
91
|
+
def project_metadata
|
92
|
+
project_folder_names.each_with_index.with_object([]) do |(project_name, index), array|
|
93
|
+
array << {
|
94
|
+
project_number: index + 1,
|
95
|
+
project_name: project_name,
|
96
|
+
current_project: project_name == current_project_name,
|
97
|
+
default_projet: project_name == default_project_name
|
98
|
+
}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def project_number_for(project_name:)
|
103
|
+
project_metadata.find do |metadata|
|
104
|
+
metadata[:project_name] == project_name
|
105
|
+
end&.[](:project_number) || -1
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def project_folder_names
|
111
|
+
Pathname.new(projects_folder)
|
112
|
+
.children
|
113
|
+
.select(&:directory?)
|
114
|
+
.map(&:basename)
|
115
|
+
.map(&:to_s)
|
116
|
+
.sort { |a, b| a.casecmp(b) }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Support
|
5
|
+
module ShortString
|
6
|
+
SHORT_STRING_MAX_COUNT = 25
|
7
|
+
|
8
|
+
module_function
|
9
|
+
|
10
|
+
def short_string(string:, count: SHORT_STRING_MAX_COUNT, elipsis: '...')
|
11
|
+
return '' if string.blank?
|
12
|
+
return string if string.length <= count
|
13
|
+
|
14
|
+
# Trim to max count and cut at the last space within the limit
|
15
|
+
trimmed_string = string[0...count].rpartition(' ')[0]
|
16
|
+
|
17
|
+
# If no space found, trim by characters
|
18
|
+
trimmed_string = string[0...(count - elipsis.length)] if trimmed_string.empty? && !string.empty?
|
19
|
+
|
20
|
+
"#{trimmed_string}#{elipsis}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Support
|
5
|
+
module TransformProjectName
|
6
|
+
TRANSFORM_PROJECT_NAME_REGEX = %r{[^/\w\s]|_}
|
7
|
+
TRANSFORM_PROJECT_NAME_SEPARATOR = '-'
|
8
|
+
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def transform_project_name(project_name, options: {})
|
12
|
+
normalized_name = project_name
|
13
|
+
.gsub(TRANSFORM_PROJECT_NAME_REGEX, ' ') # Replace non-word characters and underscores with space
|
14
|
+
.strip # Remove leading and trailing spaces
|
15
|
+
.squeeze(' ') # Convert consecutive spaces to a single space
|
16
|
+
.tr(' ', TRANSFORM_PROJECT_NAME_SEPARATOR) # Replace spaces with hyphens
|
17
|
+
.squeeze(TRANSFORM_PROJECT_NAME_SEPARATOR) # Ensure no consecutive hyphens
|
18
|
+
|
19
|
+
normalized_name.downcase! if options[:downcase]
|
20
|
+
normalized_name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../support/field_errors'
|
4
|
+
require_relative '../support/short_string'
|
5
|
+
|
6
|
+
# https://guides.rubyonrails.org/active_record_validations.html#validates-with
|
7
|
+
module Dsu
|
8
|
+
module Validators
|
9
|
+
# TODO: I18n.
|
10
|
+
class ProjectNameValidator < ActiveModel::Validator
|
11
|
+
include Support::FieldErrors
|
12
|
+
include Support::ShortString
|
13
|
+
|
14
|
+
def validate(record)
|
15
|
+
unless record.project_name.is_a?(String)
|
16
|
+
record.errors.add(:project_name, 'is the wrong object type. ' \
|
17
|
+
"\"String\" was expected, but \"#{record.project.class}\" was received.")
|
18
|
+
return
|
19
|
+
end
|
20
|
+
|
21
|
+
unless record.project_name.present?
|
22
|
+
record.errors.add(:project_name, :blank, '')
|
23
|
+
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
validate_project_name record
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validate_project_name(record)
|
33
|
+
project_name = record.project_name
|
34
|
+
|
35
|
+
return if project_name.length.between?(min_project_name_length(record), max_project_name_length(record))
|
36
|
+
|
37
|
+
if project_name.length < min_project_name_length(record)
|
38
|
+
# TODO: I18n.
|
39
|
+
record.errors.add(:project_name, "is too short: \"#{record.project_name}\" " \
|
40
|
+
"(minimum is #{min_project_name_length(record)} characters).")
|
41
|
+
elsif project_name.length > max_project_name_length(record)
|
42
|
+
# TODO: I18n.
|
43
|
+
short_project_name = short_string(string: project_name, count: max_project_name_length(record))
|
44
|
+
record.errors.add(:project_name, "is too long: \"#{short_project_name}\" " \
|
45
|
+
"(maximum is #{max_project_name_length(record)} characters).")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def min_project_name_length(record)
|
50
|
+
record.class::MIN_PROJECT_NAME_LENGTH
|
51
|
+
end
|
52
|
+
|
53
|
+
def max_project_name_length(record)
|
54
|
+
record.class::MAX_PROJECT_NAME_LENGTH
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/lib/dsu/version.rb
CHANGED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../env'
|
4
|
+
require_relative '../models/color_theme'
|
5
|
+
require_relative '../support/color_themable'
|
6
|
+
|
7
|
+
module Dsu
|
8
|
+
module Views
|
9
|
+
class BaseListView
|
10
|
+
include Support::ColorThemable
|
11
|
+
|
12
|
+
attr_reader :presenter
|
13
|
+
|
14
|
+
def initialize(presenter:, options: {})
|
15
|
+
@presenter = presenter
|
16
|
+
@options = options&.dup || {}
|
17
|
+
@color_theme = Models::ColorTheme.find(theme_name: theme_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def render
|
21
|
+
yield
|
22
|
+
rescue StandardError => e
|
23
|
+
puts apply_theme(e.message, theme_color: color_theme.error)
|
24
|
+
puts apply_theme(e.backtrace_locations.join("\n"), theme_color: color_theme.error) if Dsu.env.local?
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :color_theme, :options
|
30
|
+
|
31
|
+
def theme_name
|
32
|
+
@theme_name ||= options.fetch(:theme_name, Models::Configuration.new.theme_name)
|
33
|
+
end
|
34
|
+
|
35
|
+
def formatted_index(index:)
|
36
|
+
apply_theme("#{format('%02s', index + 1)}. ",
|
37
|
+
theme_color: color_theme.index)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|