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,118 @@
|
|
|
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 Doto
|
|
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 doto/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
|
+
return Models::Configuration::DEFAULT_CONFIGURATION[:default_project] unless Models::Configuration.exist?
|
|
43
|
+
|
|
44
|
+
Models::Configuration.new.default_project
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def initialize_project(project_name:)
|
|
48
|
+
return if project_initialized?(project_name: project_name)
|
|
49
|
+
|
|
50
|
+
# TODO: Don't know if I like this here.
|
|
51
|
+
unless current_project_file_exist?
|
|
52
|
+
file_data = {
|
|
53
|
+
version: Doto::Migration::VERSION,
|
|
54
|
+
project_name: default_project_name
|
|
55
|
+
}
|
|
56
|
+
Crud::JsonFile.write!(file_data: file_data, file_path: current_project_file)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates doto/projects/<project_name>
|
|
60
|
+
FileUtils.mkdir_p(project_folder_for(project_name: project_name))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def project_initialized?(project_name:)
|
|
64
|
+
# Checking these files, checks all the containing folders also
|
|
65
|
+
current_project_file_exist? &&
|
|
66
|
+
project_folder_exist?(project_name: project_name)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Does doto/projects/<project_name>/project.json file exist?
|
|
70
|
+
def project_file_exist?(project_name:)
|
|
71
|
+
project_file_path = project_file_for(project_name: project_name)
|
|
72
|
+
File.exist?(project_file_path)
|
|
73
|
+
end
|
|
74
|
+
alias exist? project_file_exist?
|
|
75
|
+
alias persisted? project_file_exist?
|
|
76
|
+
|
|
77
|
+
# Does doto/current_project.json file exist?
|
|
78
|
+
def current_project_file_exist?
|
|
79
|
+
File.exist?(current_project_file)
|
|
80
|
+
end
|
|
81
|
+
alias current_project_file_persisted? current_project_file_exist?
|
|
82
|
+
|
|
83
|
+
# Does doto/projects/<project_name> folder exist?
|
|
84
|
+
def project_folder_exist?(project_name:)
|
|
85
|
+
Dir.exist?(project_folder_for(project_name: project_name))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def project_metadata
|
|
89
|
+
project_folder_names.each_with_index.with_object([]) do |(project_name, index), array|
|
|
90
|
+
array << {
|
|
91
|
+
project_number: index + 1,
|
|
92
|
+
project_name: project_name,
|
|
93
|
+
current_project: project_name == current_project_name,
|
|
94
|
+
default_projet: project_name == default_project_name
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def project_number_for(project_name:)
|
|
100
|
+
project_metadata.find do |metadata|
|
|
101
|
+
metadata[:project_name] == project_name
|
|
102
|
+
end&.[](:project_number) || -1
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def project_folder_names
|
|
108
|
+
Pathname.new(projects_folder)
|
|
109
|
+
.children
|
|
110
|
+
.select(&:directory?)
|
|
111
|
+
.map(&:basename)
|
|
112
|
+
.map(&:to_s)
|
|
113
|
+
.sort { |a, b| a.casecmp(b) }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doto
|
|
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,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doto
|
|
4
|
+
module Support
|
|
5
|
+
module TimeComparable
|
|
6
|
+
TIME_COMPARABLE_FORMAT_SPECIFIER = '%Y%m%d'
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def time_equal?(other_time:)
|
|
11
|
+
time_equal_compare_string_for(time: time) == time_equal_compare_string_for(time: other_time)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def time_equal_compare_string_for(time:)
|
|
15
|
+
time = time.in_time_zone
|
|
16
|
+
|
|
17
|
+
time.strftime(TIME_COMPARABLE_FORMAT_SPECIFIER)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
require 'active_support/core_ext/numeric/time'
|
|
5
|
+
|
|
6
|
+
module Doto
|
|
7
|
+
module Support
|
|
8
|
+
# This module provides functions for formatting Time objects
|
|
9
|
+
# to display in the console.
|
|
10
|
+
module TimeFormatable
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# TODO: I18n.
|
|
14
|
+
def formatted_time(time:)
|
|
15
|
+
time = time.in_time_zone
|
|
16
|
+
|
|
17
|
+
today_yesterday_or_tomorrow = if time.today?
|
|
18
|
+
'Today'
|
|
19
|
+
elsif time.yesterday?
|
|
20
|
+
'Yesterday'
|
|
21
|
+
elsif time.tomorrow?
|
|
22
|
+
'Tomorrow'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
time_zone = timezone_for(time: time)
|
|
26
|
+
|
|
27
|
+
return time.strftime("%A, %Y-%m-%d #{time_zone}") unless today_yesterday_or_tomorrow
|
|
28
|
+
|
|
29
|
+
time.strftime("%A, (#{today_yesterday_or_tomorrow}) %Y-%m-%d #{time_zone}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# TODO: I18n.
|
|
33
|
+
def mm_dd(time:, separator: '/')
|
|
34
|
+
time.strftime("%m#{separator}%d")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# TODO: I18n.
|
|
38
|
+
def mm_dd_yyyy(time:, separator: '/')
|
|
39
|
+
time.strftime("%m#{separator}%d#{separator}%Y")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def dd_mm_yyyy(time:, separator: '/')
|
|
43
|
+
time.strftime("%d#{separator}%m#{separator}%Y")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def timezone_for(time:)
|
|
47
|
+
time.zone
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# TODO: I18n.
|
|
51
|
+
def yyyy_mm_dd_or_through_for(times:)
|
|
52
|
+
return yyyy_mm_dd(time: times[0]) if times.one?
|
|
53
|
+
|
|
54
|
+
times = [yyyy_mm_dd(time: times.min), yyyy_mm_dd(time: times.max)]
|
|
55
|
+
|
|
56
|
+
I18n.t('information.dates.through', from: times[0], to: times[1])
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# TODO: I18n.
|
|
60
|
+
def yyyy_mm_dd(time:, separator: '-')
|
|
61
|
+
time.strftime("%Y#{separator}%m#{separator}%d")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doto
|
|
4
|
+
module Support
|
|
5
|
+
module TimesSortable
|
|
6
|
+
def sorted_doto_times_for(times:)
|
|
7
|
+
configuration = Models::Configuration.new unless defined?(configuration) && configuration
|
|
8
|
+
entries_display_order = configuration.entries_display_order
|
|
9
|
+
times_sort(times: times_for(times: times), entries_display_order: entries_display_order)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def times_sort(times:, entries_display_order: nil)
|
|
13
|
+
times = times.dup
|
|
14
|
+
entries_display_order ||= :asc
|
|
15
|
+
|
|
16
|
+
validate_times_argument!(times: times)
|
|
17
|
+
validate_entries_display_order_argument!(entries_display_order: entries_display_order)
|
|
18
|
+
|
|
19
|
+
return times if times.one?
|
|
20
|
+
|
|
21
|
+
# NOTE: The times array needs to be sorted unconditionally because if
|
|
22
|
+
# the sort is ascending, then the times array needs to be returned
|
|
23
|
+
# in ascending order. If the sort is descending, then in order to
|
|
24
|
+
# properly reverse the times array, it needs to first be sorted in
|
|
25
|
+
# ascending order before being reversed.
|
|
26
|
+
return times.sort if entries_display_order == :asc
|
|
27
|
+
|
|
28
|
+
times.sort_by { |time| -time.to_i }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def times_for(times:)
|
|
32
|
+
times = times.dup
|
|
33
|
+
validate_times_argument!(times: times)
|
|
34
|
+
|
|
35
|
+
start_date = times.max
|
|
36
|
+
return times unless start_date.monday? || start_date.on_weekend?
|
|
37
|
+
|
|
38
|
+
# If the start date is a weekend or a Monday then we need to look back
|
|
39
|
+
# to include the preceeding Friday upto and including the start date.
|
|
40
|
+
(0..3).filter_map do |num|
|
|
41
|
+
time = start_date - num.days
|
|
42
|
+
next unless time == start_date || time.on_weekend? || time.friday?
|
|
43
|
+
|
|
44
|
+
time
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def validate_times_argument!(times:)
|
|
51
|
+
raise ArgumentError, "times is the wrong object type: \"#{times.class}\"" unless times.is_a?(Array)
|
|
52
|
+
raise ArgumentError, 'times is empty' if times.empty?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def validate_entries_display_order_argument!(entries_display_order:)
|
|
56
|
+
unless entries_display_order.nil? || entries_display_order.is_a?(Symbol)
|
|
57
|
+
raise ArgumentError, "entries_display_order is the wrong object type: \"#{entries_display_order.class}\""
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless %i[asc desc].include?(entries_display_order)
|
|
61
|
+
raise ArgumentError, "entries_display_order is invalid: \":#{entries_display_order}\""
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# NOTE: This, as opposed to using module_function, so that we can
|
|
66
|
+
# invoke .validate_times_sort_arguments! from the .times_sort
|
|
67
|
+
# method with module as the receiver AND when included as a mixin.
|
|
68
|
+
extend self
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doto
|
|
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,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'colorize'
|
|
4
|
+
|
|
5
|
+
# rubocop:disable Layout/LineLength
|
|
6
|
+
module Doto
|
|
7
|
+
module Validators
|
|
8
|
+
class ColorThemeValidator < ActiveModel::Validator
|
|
9
|
+
def validate(record)
|
|
10
|
+
default_theme_colors = record.class::DEFAULT_THEME_COLORS
|
|
11
|
+
|
|
12
|
+
# return unless validate_color_theme_keys!(record, default_theme_colors)
|
|
13
|
+
|
|
14
|
+
default_theme_colors.each_key do |theme_color_key|
|
|
15
|
+
theme_colors_hash = record.public_send(theme_color_key)
|
|
16
|
+
|
|
17
|
+
next unless validate_theme_color_type!(record, theme_color_key, theme_colors_hash)
|
|
18
|
+
|
|
19
|
+
if theme_colors_hash.empty?
|
|
20
|
+
record.errors.add(:base, ":#{theme_color_key} colors Hash is empty")
|
|
21
|
+
next
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
validate_theme_colors!(record, theme_colors_hash)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def colors
|
|
31
|
+
@colors ||= String.colors
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def modes
|
|
35
|
+
@modes ||= String.modes
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def validate_theme_color_type!(record, theme_color_key, theme_colors_hash)
|
|
39
|
+
return true if theme_colors_hash.is_a?(Hash)
|
|
40
|
+
|
|
41
|
+
record.errors.add(:base, ":#{theme_color_key} value is the wrong object type. " \
|
|
42
|
+
"\"Hash\" was expected, but \"#{theme_colors_hash.class}\" was received.")
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def validate_theme_colors!(record, theme_colors_hash)
|
|
47
|
+
unless colors.include?(theme_colors_hash[:color])
|
|
48
|
+
value = theme_color_value_to_s(theme_colors_hash[:color])
|
|
49
|
+
record.errors.add(:base, ":color key value #{value} in theme color Hash #{theme_colors_hash} is not a valid color. " \
|
|
50
|
+
"One of #{colors.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless theme_colors_hash[:mode].nil? || modes.include?(theme_colors_hash[:mode])
|
|
54
|
+
value = theme_color_value_to_s(theme_colors_hash[:mode])
|
|
55
|
+
record.errors.add(:base, ":mode key value #{value} in theme color Hash #{theme_colors_hash} is not a valid mode value. " \
|
|
56
|
+
"One of #{modes.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
unless theme_colors_hash[:background].nil? || colors.include?(theme_colors_hash[:background])
|
|
60
|
+
value = theme_color_value_to_s(theme_colors_hash[:background])
|
|
61
|
+
record.errors.add(:base, ":background key value #{value} in theme color Hash #{theme_colors_hash} is not a valid color. " \
|
|
62
|
+
"One of #{colors.wrap_and_join(wrapper: [':', ''])} was expected, but #{value} was received.")
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def theme_color_value_to_s(theme_color_value)
|
|
67
|
+
return ":#{theme_color_value}" if theme_color_value.is_a?(Symbol)
|
|
68
|
+
|
|
69
|
+
"'#{theme_color_value}'"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
# rubocop:enable Layout/LineLength
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Doto
|
|
4
|
+
module Validators
|
|
5
|
+
class DescriptionValidator < ActiveModel::Validator
|
|
6
|
+
def validate(record)
|
|
7
|
+
description = record.description
|
|
8
|
+
|
|
9
|
+
if description.blank?
|
|
10
|
+
record.errors.add(:description, :blank)
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
unless description.is_a?(String)
|
|
15
|
+
# TODO: I18n.
|
|
16
|
+
record.errors.add(:description, 'is the wrong object type. ' \
|
|
17
|
+
"\"String\" was expected, but \"#{description.class}\" was received.")
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
validate_description record
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def validate_description(record)
|
|
27
|
+
description = record.description
|
|
28
|
+
|
|
29
|
+
return if description.length.between?(min_description_length(record), max_description_length(record))
|
|
30
|
+
|
|
31
|
+
if description.length < min_description_length(record)
|
|
32
|
+
# TODO: I18n.
|
|
33
|
+
record.errors.add(:description, "is too short: \"#{record.short_description}\" " \
|
|
34
|
+
"(minimum is #{min_description_length(record)} characters).")
|
|
35
|
+
elsif description.length > max_description_length(record)
|
|
36
|
+
# TODO: I18n.
|
|
37
|
+
record.errors.add(:description, "is too long: \"#{record.short_description}\" " \
|
|
38
|
+
"(maximum is #{max_description_length(record)} characters).")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def min_description_length(record)
|
|
43
|
+
record.class::MIN_DESCRIPTION_LENGTH
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def max_description_length(record)
|
|
47
|
+
record.class::MAX_DESCRIPTION_LENGTH
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../models/entry'
|
|
4
|
+
require_relative '../support/field_errors'
|
|
5
|
+
|
|
6
|
+
# https://guides.rubyonrails.org/active_record_validations.html#validates-with
|
|
7
|
+
module Doto
|
|
8
|
+
module Validators
|
|
9
|
+
# TODO: I18n.
|
|
10
|
+
class EntriesValidator < ActiveModel::Validator
|
|
11
|
+
include Support::FieldErrors
|
|
12
|
+
|
|
13
|
+
def validate(record)
|
|
14
|
+
unless record.entries.is_a?(Array)
|
|
15
|
+
record.errors.add(:entries, 'is the wrong object type. ' \
|
|
16
|
+
"\"Array\" was expected, but \"#{record.entries.class}\" was received.")
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
return if validate_entry_types record
|
|
21
|
+
|
|
22
|
+
validate_unique_entry record
|
|
23
|
+
validate_entries record
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def validate_entry_types(record)
|
|
29
|
+
record.entries.each do |entry|
|
|
30
|
+
next if entry.is_a? Doto::Models::Entry
|
|
31
|
+
|
|
32
|
+
record.errors.add(:entries, 'Array element is the wrong object type. ' \
|
|
33
|
+
"\"Entry\" was expected, but \"#{entry.class}\" was received.",
|
|
34
|
+
type: Support::FieldErrors::FIELD_TYPE_ERROR)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
record.errors.any?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def validate_unique_entry(record)
|
|
41
|
+
return unless record.entries.is_a? Array
|
|
42
|
+
|
|
43
|
+
entry_objects = record.entries.select { |entry| entry.is_a?(Doto::Models::Entry) }
|
|
44
|
+
|
|
45
|
+
descriptions = entry_objects.map(&:description)
|
|
46
|
+
return if descriptions.uniq.length == descriptions.length
|
|
47
|
+
|
|
48
|
+
non_unique_descriptions = descriptions.select { |description| descriptions.count(description) > 1 }.uniq
|
|
49
|
+
non_unique_descriptions.each do |non_unique_description|
|
|
50
|
+
# TODO: I18n.
|
|
51
|
+
record.errors.add(:entries,
|
|
52
|
+
"array contains duplicate entry: \"#{short_description(non_unique_description)}\".",
|
|
53
|
+
type: Support::FieldErrors::FIELD_DUPLICATE_ERROR)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
record.errors.any?
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def validate_entries(record)
|
|
60
|
+
entries = record.entries
|
|
61
|
+
return if entries.none?
|
|
62
|
+
|
|
63
|
+
entries.each do |entry|
|
|
64
|
+
next if entry.valid?
|
|
65
|
+
|
|
66
|
+
entry.errors.each do |error|
|
|
67
|
+
record.errors.add(:entries_entry, error.full_message)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def short_description(description)
|
|
73
|
+
Models::Entry.short_description(string: description)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
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 Doto
|
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# https://guides.rubyonrails.org/active_record_validations.html#validates-with
|
|
4
|
+
module Doto
|
|
5
|
+
module Validators
|
|
6
|
+
class TimeValidator < ActiveModel::Validator
|
|
7
|
+
def validate(record)
|
|
8
|
+
time = record.time
|
|
9
|
+
|
|
10
|
+
if time.nil?
|
|
11
|
+
record.errors.add(:time, :blank)
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
unless time.is_a?(Time)
|
|
16
|
+
record.errors.add(:time, 'is the wrong object type. ' \
|
|
17
|
+
"\"Time\" was expected, but \"#{time.class}\" was received.")
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
record.errors.add(:time, 'is not in localtime format.') unless time == time.in_time_zone
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# https://guides.rubyonrails.org/active_record_validations.html#validates-with
|
|
4
|
+
module Doto
|
|
5
|
+
module Validators
|
|
6
|
+
class VersionValidator < ActiveModel::Validator
|
|
7
|
+
def validate(record)
|
|
8
|
+
version = record.version
|
|
9
|
+
|
|
10
|
+
if version.nil?
|
|
11
|
+
record.errors.add(:version, 'is nil')
|
|
12
|
+
return
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
unless version.is_a?(Integer)
|
|
16
|
+
record.errors.add(:version, 'is the wrong object type. ' \
|
|
17
|
+
"\"Integer\" was expected, but \"#{version.class}\" was received.")
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# TODO: This validation should check the configuration version
|
|
22
|
+
# against the current migration version and they should match.
|
|
23
|
+
# unless version == record.class::VERSION
|
|
24
|
+
# record.errors.add(:version, "\"#{version}\" is not the correct version: \"#{record.class::VERSION}\"")
|
|
25
|
+
# end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|