dsu 0.1.0.alpha.5 → 1.1.0.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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +16 -0
- data/Gemfile.lock +6 -15
- data/README.md +33 -47
- data/lib/dsu/base_cli.rb +13 -6
- data/lib/dsu/cli.rb +46 -55
- data/lib/dsu/command_services/add_entry_service.rb +21 -21
- data/lib/dsu/core/ruby/not_today.rb +11 -0
- data/lib/dsu/models/entry.rb +32 -21
- data/lib/dsu/models/entry_group.rb +41 -105
- data/lib/dsu/services/configuration_loader_service.rb +19 -2
- data/lib/dsu/services/entry_group_editor_service.rb +37 -89
- data/lib/dsu/services/stdout_redirector_service.rb +27 -0
- data/lib/dsu/subcommands/list.rb +83 -15
- data/lib/dsu/support/colorable.rb +1 -0
- data/lib/dsu/support/command_options/dsu_times.rb +33 -0
- data/lib/dsu/support/command_options/time.rb +77 -0
- data/lib/dsu/support/command_options/time_mneumonic.rb +127 -0
- data/lib/dsu/support/command_options/time_mneumonics.rb +15 -0
- data/lib/dsu/support/configurable.rb +15 -0
- data/lib/dsu/support/configuration.rb +13 -1
- data/lib/dsu/support/entry_group_fileable.rb +31 -6
- data/lib/dsu/support/entry_group_loadable.rb +13 -16
- data/lib/dsu/support/entry_group_viewable.rb +26 -8
- data/lib/dsu/support/times_sortable.rb +1 -3
- data/lib/dsu/validators/description_validator.rb +38 -0
- data/lib/dsu/validators/entries_validator.rb +43 -32
- data/lib/dsu/validators/time_validator.rb +11 -20
- data/lib/dsu/version.rb +1 -1
- data/lib/dsu/views/edited_entries/shared/errors.rb +39 -0
- data/lib/dsu/views/entry_group/edit.rb +89 -39
- data/lib/dsu/views/entry_group/show.rb +10 -4
- data/lib/dsu/views/shared/messages.rb +56 -0
- data/lib/dsu.rb +8 -2
- metadata +24 -12
- data/lib/dsu/support/commander/command.rb +0 -130
- data/lib/dsu/support/commander/command_help.rb +0 -62
- data/lib/dsu/support/commander/subcommand.rb +0 -45
- data/lib/dsu/support/interactive/cli.rb +0 -161
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Support
|
5
|
+
module CommandOptions
|
6
|
+
# The purpose of this module is to take a command option that is a string and return a Time object.
|
7
|
+
# The command option is expected to be a date in the format of [M]M/[D]D[/YYYY]. MM and DD with
|
8
|
+
# leading zeroes is optional (i.e. only M and D are required), YYYY is optionl and will be replaced
|
9
|
+
# with the current year if not provided.
|
10
|
+
module Time
|
11
|
+
DATE_CAPTURE_REGEX = %r{\A(?<month>0?[1-9]|1[0-2])/(?<day>0?[1-9]|1\d|2\d|3[01])(?:/(?<year>\d{4}))?\z}
|
12
|
+
|
13
|
+
def time_from_date_string!(command_option:)
|
14
|
+
raise ArgumentError, 'command_option is nil.' if command_option.nil?
|
15
|
+
raise ArgumentError, 'command_option is blank.' if command_option.blank?
|
16
|
+
|
17
|
+
unless command_option.is_a?(String)
|
18
|
+
raise ArgumentError, "command_option is not a String: \"#{command_option}\"."
|
19
|
+
end
|
20
|
+
|
21
|
+
time_parts = time_parts_for(time_string: command_option)
|
22
|
+
return unless time_parts?(time_parts: time_parts)
|
23
|
+
|
24
|
+
valid_time!(time_parts: time_parts)
|
25
|
+
|
26
|
+
# This will rescue errors resulting from calling Date.strptime with an invalid date string,
|
27
|
+
# and return a more meaningful error message.
|
28
|
+
rescue DateTime::Error
|
29
|
+
raise ArgumentError, "command_option is not a valid date: \"#{command_option}\"."
|
30
|
+
end
|
31
|
+
|
32
|
+
def time_from_date_string(command_option:)
|
33
|
+
time_from_date_string!(command_option: command_option)
|
34
|
+
rescue ArgumentError
|
35
|
+
nil
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
# This method returns the time parts for the given time string in a hash
|
41
|
+
# (i.e. month, day, year) IF the time string matches the DATE_CAPTURE_REGEX
|
42
|
+
# regex. Otherwise, it returns an empty hash.
|
43
|
+
def time_parts_for(time_string:)
|
44
|
+
match_data = DATE_CAPTURE_REGEX.match(time_string)
|
45
|
+
return {} if match_data.nil?
|
46
|
+
|
47
|
+
{
|
48
|
+
month: match_data[:month],
|
49
|
+
day: match_data[:day],
|
50
|
+
year: match_data[:year]
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# This method returns true if the date passes the DATE_CAPTURE_REGEX regex match
|
55
|
+
# in #date_parts_for and returns a non-nil hash. Otherwise, it returns false.
|
56
|
+
# A non-nil hash returned from #date_parts_for doesn necessarily mean the date
|
57
|
+
# parts will equate to a valid date when parsed, it just means the date string
|
58
|
+
# matched the regex. Calling #valid_date! will raise an ArgumentError if the
|
59
|
+
# date parts do not equate to a valid date.
|
60
|
+
def time_parts?(time_parts:)
|
61
|
+
!time_parts.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def valid_time!(time_parts:)
|
65
|
+
time_string = time_string_for(time_parts: time_parts)
|
66
|
+
Date.strptime(time_string, '%Y/%m/%d').to_time
|
67
|
+
end
|
68
|
+
|
69
|
+
def time_string_for(time_parts:)
|
70
|
+
# Replace the year with the current year if it is nil.
|
71
|
+
time_parts[:year] = ::Time.now.year if time_parts[:year].nil?
|
72
|
+
"#{time_parts[:year]}/#{time_parts[:month]}/#{time_parts[:day]}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'time_mneumonics'
|
4
|
+
|
5
|
+
module Dsu
|
6
|
+
module Support
|
7
|
+
module CommandOptions
|
8
|
+
# The purpose of this module is to take a command option that is a string and return a Time object.
|
9
|
+
# The command option is expected to be a time mneumoic.
|
10
|
+
module TimeMneumonic
|
11
|
+
include TimeMneumonics
|
12
|
+
|
13
|
+
def time_from_mneumonic(command_option:, relative_time: nil)
|
14
|
+
time_from_mneumonic!(command_option: command_option, relative_time: relative_time)
|
15
|
+
rescue ArgumentError
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
# command_option: is expected to me a time mneumonic. If relative_time is NOT nil, all
|
20
|
+
# time mneumonics are relative to relative_time. Otherwise, they are relative to Time.now.
|
21
|
+
# relative_time: is a Time object that is required IF command_option is expected to be
|
22
|
+
# a relative time mneumonic. Otherwise, it is optional.
|
23
|
+
def time_from_mneumonic!(command_option:, relative_time: nil)
|
24
|
+
validate_argument!(command_option: command_option, command_option_name: :command_option)
|
25
|
+
unless relative_time.nil?
|
26
|
+
validate_argument!(command_option: relative_time, command_option_name: :relative_time)
|
27
|
+
end
|
28
|
+
|
29
|
+
# if relative_time_mneumonic?(command_option) && !relative_time.nil?
|
30
|
+
# # If command_option is a relative time mneumonic, we need to get the time
|
31
|
+
# # relative to relative_time using ::Time.now, and use the command_option
|
32
|
+
# # as the relative time.
|
33
|
+
# return time_from_mneumonic!(command_option: relative_time, relative_time: command_option)
|
34
|
+
# end
|
35
|
+
|
36
|
+
time_for_mneumonic(mneumonic: command_option, relative_time: relative_time)
|
37
|
+
end
|
38
|
+
|
39
|
+
# This method returns true if mneumonic is a valid mneumonic OR
|
40
|
+
# a relative time mneumonic.
|
41
|
+
def time_mneumonic?(mneumonic)
|
42
|
+
mneumonic?(mneumonic) || relative_time_mneumonic?(mneumonic)
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Returns a Time object from a mneumonic.
|
48
|
+
def time_for_mneumonic(mneumonic:, relative_time: nil)
|
49
|
+
# If relative_time is a relative time mneumonic, then we need to first
|
50
|
+
# convert mneumonic to a Time object first, so that we can calculate
|
51
|
+
# `relative_time.to_i.days.from_now(time)` to get the correct Time we
|
52
|
+
# need.
|
53
|
+
if relative_time_mneumonic?(relative_time)
|
54
|
+
time = time_for_mneumonic(mneumonic: mneumonic)
|
55
|
+
return relative_time_for(days_from_now: relative_time, time: time)
|
56
|
+
end
|
57
|
+
|
58
|
+
if mneumonic?(mneumonic) && mneumonic?(relative_time)
|
59
|
+
time = time_for_mneumonic(mneumonic: mneumonic)
|
60
|
+
|
61
|
+
# Simply return the time if relative_time is 'today'
|
62
|
+
# because 'today' relative to any time will always
|
63
|
+
# point to itself.
|
64
|
+
return time if today_mneumonic?(relative_time)
|
65
|
+
|
66
|
+
return time.public_send(relative_time)
|
67
|
+
end
|
68
|
+
|
69
|
+
time = ::Time.now
|
70
|
+
if today_mneumonic?(mneumonic)
|
71
|
+
time
|
72
|
+
elsif tomorrow_mneumonic?(mneumonic)
|
73
|
+
time.tomorrow
|
74
|
+
elsif yesterday_mneumonic?(mneumonic)
|
75
|
+
time.yesterday
|
76
|
+
elsif relative_time_mneumonic?(mneumonic)
|
77
|
+
relative_time_for(days_from_now: mneumonic, time: time)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def relative_time_for(days_from_now:, time:)
|
82
|
+
days_from_now.to_i.days.from_now(time)
|
83
|
+
end
|
84
|
+
|
85
|
+
# This method returns true if mneumonic is a valid time mneumonic.
|
86
|
+
# This method will return false if mneumonic is an invalid mneumonic
|
87
|
+
# OR if mneumonic is a relative time mneumonic.
|
88
|
+
def mneumonic?(mneumonic)
|
89
|
+
today_mneumonic?(mneumonic) ||
|
90
|
+
tomorrow_mneumonic?(mneumonic) ||
|
91
|
+
yesterday_mneumonic?(mneumonic)
|
92
|
+
end
|
93
|
+
|
94
|
+
# This method returns true if mneumonic is a valid relative
|
95
|
+
# time mneumonic.
|
96
|
+
def relative_time_mneumonic?(mneumonic)
|
97
|
+
return false unless mneumonic.is_a?(String)
|
98
|
+
|
99
|
+
mneumonic.match?(RELATIVE_REGEX)
|
100
|
+
end
|
101
|
+
|
102
|
+
def today_mneumonic?(mneumonic)
|
103
|
+
TODAY.include?(mneumonic)
|
104
|
+
end
|
105
|
+
|
106
|
+
def tomorrow_mneumonic?(mneumonic)
|
107
|
+
TOMORROW.include?(mneumonic)
|
108
|
+
end
|
109
|
+
|
110
|
+
def yesterday_mneumonic?(mneumonic)
|
111
|
+
YESERDAY.include?(mneumonic)
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_argument!(command_option:, command_option_name:)
|
115
|
+
raise ArgumentError, "#{command_option_name} cannot be nil." if command_option.nil?
|
116
|
+
raise ArgumentError, "#{command_option_name} cannot be blank." if command_option.blank?
|
117
|
+
unless command_option.is_a?(String)
|
118
|
+
raise ArgumentError, "#{command_option_name} must be a String: \"#{command_option}\""
|
119
|
+
end
|
120
|
+
unless time_mneumonic?(command_option)
|
121
|
+
raise ArgumentError, "#{command_option_name} is an invalid mneumonic: \"#{command_option}\"."
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Support
|
5
|
+
module CommandOptions
|
6
|
+
module TimeMneumonics
|
7
|
+
TODAY = %w[n today].freeze
|
8
|
+
TOMORROW = %w[t tomorrow].freeze
|
9
|
+
YESERDAY = %w[y yesterday].freeze
|
10
|
+
|
11
|
+
RELATIVE_REGEX = /[+-]\d+/
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../services/configuration_loader_service'
|
4
|
+
|
5
|
+
module Dsu
|
6
|
+
module Support
|
7
|
+
# This module provides a way to configure a class, so that it can
|
8
|
+
# be used in a test environment.
|
9
|
+
module Configurable
|
10
|
+
def configuration
|
11
|
+
@configuration ||= Services::ConfigurationLoaderService.new.call
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -27,7 +27,19 @@ module Dsu
|
|
27
27
|
# asc or desc, ascending or descending, respectively.
|
28
28
|
'entries_display_order' => 'desc',
|
29
29
|
'entries_file_name' => '%Y-%m-%d.json',
|
30
|
-
'entries_folder' => "#{FolderLocations.root_folder}/dsu/entries"
|
30
|
+
'entries_folder' => "#{FolderLocations.root_folder}/dsu/entries",
|
31
|
+
'carry_over_entries_to_today' => false,
|
32
|
+
# If true, when using dsu commands that list date ranges (e.g.
|
33
|
+
# `dsu list dates`), the displayed list will include dates that
|
34
|
+
# have no dsu entries. If false, the displayed list will only
|
35
|
+
# include dates that have dsu entries.
|
36
|
+
# For all other `dsu list` commands, if true, this option will
|
37
|
+
# behave in the aforementioned manner. If false, the displayed
|
38
|
+
# list will unconditionally display the first and last dates
|
39
|
+
# regardless of whether or not the DSU date has entries or not;
|
40
|
+
# all other dates will not be displayed if the DSU date has no
|
41
|
+
# entries.
|
42
|
+
'include_all' => false
|
31
43
|
}.freeze
|
32
44
|
# rubocop:enable Style/StringHashKeys
|
33
45
|
|
@@ -1,14 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative '../services/configuration_loader_service'
|
4
|
+
require_relative '../support/configurable'
|
4
5
|
|
5
6
|
module Dsu
|
6
7
|
module Support
|
8
|
+
# TODO: I hate this module; refactor it!!!
|
9
|
+
# This module expects the following attributes to be defined: :time, :options
|
7
10
|
module EntryGroupFileable
|
8
|
-
|
11
|
+
extend Support::Configurable
|
12
|
+
|
13
|
+
class << self
|
14
|
+
def entry_group_file_exists?(time:)
|
15
|
+
File.exist?(entry_group_file_path(time: time))
|
16
|
+
end
|
17
|
+
|
18
|
+
def entry_group_file_path(time:)
|
19
|
+
File.join(entries_folder, entries_file_name(time: time))
|
20
|
+
end
|
21
|
+
|
22
|
+
def entries_folder
|
23
|
+
configuration[:entries_folder]
|
24
|
+
end
|
25
|
+
|
26
|
+
def entries_file_name(time:)
|
27
|
+
time.strftime(configuration[:entries_file_name])
|
28
|
+
end
|
29
|
+
|
30
|
+
# def configuration
|
31
|
+
# Services::ConfigurationLoaderService.new.call
|
32
|
+
# end
|
33
|
+
end
|
9
34
|
|
10
35
|
def entry_group_file_exists?
|
11
|
-
|
36
|
+
EntryGroupFileable.entry_group_file_exists?(time: time)
|
12
37
|
end
|
13
38
|
|
14
39
|
def entry_group_path_exists?
|
@@ -16,15 +41,15 @@ module Dsu
|
|
16
41
|
end
|
17
42
|
|
18
43
|
def entry_group_file_path
|
19
|
-
|
44
|
+
EntryGroupFileable.entry_group_file_path(time: time)
|
20
45
|
end
|
21
46
|
|
22
47
|
def entries_folder
|
23
|
-
@entries_folder ||=
|
48
|
+
@entries_folder ||= EntryGroupFileable.entries_folder
|
24
49
|
end
|
25
50
|
|
26
51
|
def entries_file_name
|
27
|
-
@entries_file_name ||=
|
52
|
+
@entries_file_name ||= EntryGroupFileable.entries_file_name(time: time)
|
28
53
|
end
|
29
54
|
|
30
55
|
def create_entry_group_path_if!
|
@@ -34,7 +59,7 @@ module Dsu
|
|
34
59
|
private
|
35
60
|
|
36
61
|
def configuration
|
37
|
-
@configuration ||= options[:configuration] ||
|
62
|
+
@configuration ||= options[:configuration] || EntryGroupFileable.configuration
|
38
63
|
end
|
39
64
|
end
|
40
65
|
end
|
@@ -3,30 +3,27 @@
|
|
3
3
|
require 'pathname'
|
4
4
|
require_relative '../services/entry_group_reader_service'
|
5
5
|
require_relative '../models/entry'
|
6
|
+
require_relative '../models/entry_group'
|
6
7
|
|
7
8
|
module Dsu
|
8
9
|
module Support
|
9
10
|
module EntryGroupLoadable
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
# where entries == an Array of Entry Hashes
|
14
|
-
# representing the JSON Entry objects for :time.
|
15
|
-
def entry_group_hash_for(time:)
|
11
|
+
# returns an EntryGroup object loaded from
|
12
|
+
# the entry group json file.
|
13
|
+
def load(time:)
|
16
14
|
entry_group_json = Services::EntryGroupReaderService.new(time: time).call
|
17
|
-
if entry_group_json.present?
|
18
|
-
|
15
|
+
hash = if entry_group_json.present?
|
16
|
+
JSON.parse(entry_group_json, symbolize_names: true).tap do |hash|
|
19
17
|
hash[:time] = Time.parse(hash[:time])
|
20
18
|
end
|
19
|
+
else
|
20
|
+
{ time: time, entries: [] }
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
time: time,
|
25
|
-
entries: []
|
26
|
-
}
|
23
|
+
Models::EntryGroup.new(**hydrate_entry_group_hash(hash: hash, time: time))
|
27
24
|
end
|
28
25
|
|
29
|
-
|
26
|
+
module_function
|
30
27
|
|
31
28
|
# Accepts an entry group hash and returns a
|
32
29
|
# hydrated entry group hash:
|
@@ -39,10 +36,10 @@ module Dsu
|
|
39
36
|
# ...
|
40
37
|
# ]
|
41
38
|
# }
|
42
|
-
def hydrate_entry_group_hash(
|
43
|
-
time =
|
39
|
+
def hydrate_entry_group_hash(hash:, time:)
|
40
|
+
time = hash.fetch(:time, time)
|
44
41
|
time = Time.parse(time) unless time.is_a? Time
|
45
|
-
entries =
|
42
|
+
entries = hash.fetch(:entries, [])
|
46
43
|
entries = entries.map { |entry_hash| Models::Entry.new(**entry_hash) }
|
47
44
|
|
48
45
|
{ time: time, entries: entries }
|
@@ -3,17 +3,35 @@
|
|
3
3
|
module Dsu
|
4
4
|
module Support
|
5
5
|
module EntryGroupViewable
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
6
|
+
def view_entry_groups(times:, options: {})
|
7
|
+
total_viewable_entry_groups = 0
|
8
|
+
|
9
|
+
times.each do |time|
|
10
|
+
view_entry_group(time: time, options: options) do
|
11
|
+
total_viewable_entry_groups += 1
|
12
|
+
puts
|
13
|
+
end
|
14
14
|
end
|
15
|
+
|
16
|
+
yield total_viewable_entry_groups if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def view_entry_group(time:, options: {})
|
20
|
+
return unless show_entry_group?(time: time, options: options)
|
21
|
+
|
22
|
+
entry_group = Models::EntryGroup.load(time: time)
|
15
23
|
Views::EntryGroup::Show.new(entry_group: entry_group).render
|
24
|
+
|
25
|
+
yield if block_given?
|
16
26
|
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def show_entry_group?(time:, options:)
|
31
|
+
Models::EntryGroup.exists?(time: time) || options[:include_all]
|
32
|
+
end
|
33
|
+
|
34
|
+
module_function :view_entry_group, :view_entry_groups, :show_entry_group?
|
17
35
|
end
|
18
36
|
end
|
19
37
|
end
|
@@ -18,13 +18,11 @@ module Dsu
|
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
# TODO: Do we have something else we can use here?
|
21
22
|
def times_for(times:)
|
22
23
|
start_date = times.max
|
23
24
|
return times unless start_date.monday? || start_date.on_weekend?
|
24
25
|
|
25
|
-
# (0..3).map { |num| start_date - num.days }
|
26
|
-
# (start_date..-start_date.friday?).map { |time| time }
|
27
|
-
# (0..3).map { |num| start_date - num.days if start_date.on_weekend? || start_date.monday? }
|
28
26
|
# If the start_date is a weekend or a Monday, then we need to include
|
29
27
|
# start_date along with all the dates up to and including the previous
|
30
28
|
# Monday.
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
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
|
+
record.errors.add(field, 'is the wrong object type. ' \
|
16
|
+
"\"String\" was expected, but \"#{description.class}\" was received.")
|
17
|
+
return
|
18
|
+
end
|
19
|
+
|
20
|
+
validate_description record
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate_description(record)
|
26
|
+
description = record.description
|
27
|
+
|
28
|
+
return if description.length.between?(2, 256)
|
29
|
+
|
30
|
+
if description.length < 2
|
31
|
+
record.errors.add(:description, "is too short: \"#{record.short_description}\" (minimum is 2 characters).")
|
32
|
+
elsif description.length > 256
|
33
|
+
record.errors.add(:description, "is too long: \"#{record.short_description}\" (maximum is 256 characters).")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -7,56 +7,67 @@ require_relative '../support/field_errors'
|
|
7
7
|
module Dsu
|
8
8
|
module Validators
|
9
9
|
class EntriesValidator < ActiveModel::Validator
|
10
|
-
include
|
10
|
+
include Support::FieldErrors
|
11
11
|
|
12
12
|
def validate(record)
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
unless record.entries.is_a?(Array)
|
14
|
+
record.errors.add(:entries_entry, 'is the wrong object type. ' \
|
15
|
+
"\"Array\" was expected, but \"#{record.entries.class}\" was received.")
|
16
|
+
end
|
16
17
|
|
17
|
-
|
18
|
-
|
18
|
+
validate_entry_types record
|
19
|
+
validate_unique_entry record
|
20
|
+
validate_entries record
|
21
|
+
end
|
19
22
|
|
20
|
-
|
21
|
-
record.errors.add(field, 'is the wrong object type. ' \
|
22
|
-
"\"Array\" was expected, but \"#{entries.class}\" was received.")
|
23
|
-
next
|
24
|
-
end
|
23
|
+
private
|
25
24
|
|
26
|
-
|
27
|
-
|
28
|
-
|
25
|
+
def validate_entry_types(record)
|
26
|
+
record.entries.each do |entry|
|
27
|
+
next if entry.is_a? Dsu::Models::Entry
|
28
|
+
|
29
|
+
record.errors.add(:entries_entry, 'entry Array element is the wrong object type. ' \
|
30
|
+
"\"Entry\" was expected, but \"#{entry.class}\" was received.",
|
31
|
+
type: Support::FieldErrors::FIELD_TYPE_ERROR)
|
29
32
|
end
|
30
33
|
end
|
31
34
|
|
32
|
-
|
35
|
+
def validate_unique_entry(record)
|
36
|
+
return unless record.entries.is_a? Array
|
33
37
|
|
34
|
-
|
35
|
-
entries.each do |entry|
|
36
|
-
next if entry.is_a? Dsu::Models::Entry
|
38
|
+
entry_objects = record.entries.select { |entry| entry.is_a?(Dsu::Models::Entry) }
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
type: Dsu::Support::FieldErrors::FIELD_TYPE_ERROR)
|
40
|
+
descriptions = entry_objects.map(&:description)
|
41
|
+
return if descriptions.uniq.length == descriptions.length
|
41
42
|
|
42
|
-
|
43
|
+
non_unique_descriptions = descriptions.select { |description| descriptions.count(description) > 1 }.uniq
|
44
|
+
if non_unique_descriptions.any?
|
45
|
+
record.errors.add(:entries_entry, 'contains a duplicate entry: ' \
|
46
|
+
"#{format_non_unique_descriptions(non_unique_descriptions)}.",
|
47
|
+
type: Support::FieldErrors::FIELD_DUPLICATE_ERROR)
|
43
48
|
end
|
44
49
|
end
|
45
50
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
entry_objects = entries.select { |entry| entry.is_a?(Dsu::Models::Entry) }
|
51
|
+
def validate_entries(record)
|
52
|
+
entries = record.entries
|
53
|
+
return if entries.none?
|
50
54
|
|
51
|
-
|
52
|
-
|
55
|
+
entries.each do |entry|
|
56
|
+
next if entry.valid?
|
53
57
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
type: Dsu::Support::FieldErrors::FIELD_DUPLICATE_ERROR)
|
58
|
+
entry.errors.each do |error|
|
59
|
+
record.errors.add(:entries_entry, error.full_message)
|
60
|
+
end
|
58
61
|
end
|
59
62
|
end
|
63
|
+
|
64
|
+
def format_non_unique_descriptions(non_unique_descriptions)
|
65
|
+
non_unique_descriptions.map { |description| "\"#{short_description(description)}\"" }.join(', ')
|
66
|
+
end
|
67
|
+
|
68
|
+
def short_description(description)
|
69
|
+
Models::Entry.short_description(string: description)
|
70
|
+
end
|
60
71
|
end
|
61
72
|
end
|
62
73
|
end
|
@@ -5,29 +5,20 @@ module Dsu
|
|
5
5
|
module Validators
|
6
6
|
class TimeValidator < ActiveModel::Validator
|
7
7
|
def validate(record)
|
8
|
-
|
9
|
-
raise 'options[:fields] is not an Array.' unless options[:fields].is_a? Array
|
10
|
-
raise 'options[:fields] elements are not Symbols.' unless options[:fields].all?(Symbol)
|
8
|
+
time = record.time
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
record.errors.add(field, :blank)
|
17
|
-
next
|
18
|
-
end
|
19
|
-
|
20
|
-
unless time.is_a?(Time)
|
21
|
-
record.errors.add(field, 'is the wrong object type. ' \
|
22
|
-
"\"Time\" was expected, but \"#{time.class}\" was received.")
|
23
|
-
next
|
24
|
-
end
|
10
|
+
if time.nil?
|
11
|
+
record.errors.add(:time, :blank)
|
12
|
+
return
|
13
|
+
end
|
25
14
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
30
19
|
end
|
20
|
+
|
21
|
+
record.errors.add(:time, 'is not in localtime format.') if time.utc?
|
31
22
|
end
|
32
23
|
end
|
33
24
|
end
|
data/lib/dsu/version.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../shared/messages'
|
4
|
+
|
5
|
+
module Dsu
|
6
|
+
module Views
|
7
|
+
module EditedEntries
|
8
|
+
module Shared
|
9
|
+
class Errors
|
10
|
+
def initialize(edited_entries:, options: {})
|
11
|
+
raise ArgumentError, 'edited_entries is nil' if edited_entries.nil?
|
12
|
+
raise ArgumentError, 'edited_entries is the wrong object type' unless edited_entries.is_a?(Array)
|
13
|
+
unless edited_entries.all?(Models::EditedEntry)
|
14
|
+
raise ArgumentError, 'edited_entries elements are the wrong object type'
|
15
|
+
end
|
16
|
+
raise ArgumentError, 'options is nil' if options.nil?
|
17
|
+
raise ArgumentError, 'options is the wrong object type' unless options.is_a?(Hash)
|
18
|
+
|
19
|
+
@edited_entries = edited_entries
|
20
|
+
@options = options || {}
|
21
|
+
@header = options[:header] || 'The following ERRORS were encountered; these changes were not saved:'
|
22
|
+
end
|
23
|
+
|
24
|
+
def render
|
25
|
+
return if edited_entries.empty?
|
26
|
+
return if edited_entries.all?(&:valid?)
|
27
|
+
|
28
|
+
messages = edited_entries.map { |edited_entry| edited_entry.errors.full_messages }.flatten
|
29
|
+
Views::Shared::Messages.new(messages: messages, message_type: :error, options: { header: header }).render
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
attr_reader :edited_entries, :header, :options
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|