dsu 0.1.0.alpha.5 → 1.1.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|