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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +16 -0
  4. data/Gemfile.lock +6 -15
  5. data/README.md +33 -47
  6. data/lib/dsu/base_cli.rb +13 -6
  7. data/lib/dsu/cli.rb +46 -55
  8. data/lib/dsu/command_services/add_entry_service.rb +21 -21
  9. data/lib/dsu/core/ruby/not_today.rb +11 -0
  10. data/lib/dsu/models/entry.rb +32 -21
  11. data/lib/dsu/models/entry_group.rb +41 -105
  12. data/lib/dsu/services/configuration_loader_service.rb +19 -2
  13. data/lib/dsu/services/entry_group_editor_service.rb +37 -89
  14. data/lib/dsu/services/stdout_redirector_service.rb +27 -0
  15. data/lib/dsu/subcommands/list.rb +83 -15
  16. data/lib/dsu/support/colorable.rb +1 -0
  17. data/lib/dsu/support/command_options/dsu_times.rb +33 -0
  18. data/lib/dsu/support/command_options/time.rb +77 -0
  19. data/lib/dsu/support/command_options/time_mneumonic.rb +127 -0
  20. data/lib/dsu/support/command_options/time_mneumonics.rb +15 -0
  21. data/lib/dsu/support/configurable.rb +15 -0
  22. data/lib/dsu/support/configuration.rb +13 -1
  23. data/lib/dsu/support/entry_group_fileable.rb +31 -6
  24. data/lib/dsu/support/entry_group_loadable.rb +13 -16
  25. data/lib/dsu/support/entry_group_viewable.rb +26 -8
  26. data/lib/dsu/support/times_sortable.rb +1 -3
  27. data/lib/dsu/validators/description_validator.rb +38 -0
  28. data/lib/dsu/validators/entries_validator.rb +43 -32
  29. data/lib/dsu/validators/time_validator.rb +11 -20
  30. data/lib/dsu/version.rb +1 -1
  31. data/lib/dsu/views/edited_entries/shared/errors.rb +39 -0
  32. data/lib/dsu/views/entry_group/edit.rb +89 -39
  33. data/lib/dsu/views/entry_group/show.rb +10 -4
  34. data/lib/dsu/views/shared/messages.rb +56 -0
  35. data/lib/dsu.rb +8 -2
  36. metadata +24 -12
  37. data/lib/dsu/support/commander/command.rb +0 -130
  38. data/lib/dsu/support/commander/command_help.rb +0 -62
  39. data/lib/dsu/support/commander/subcommand.rb +0 -45
  40. 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
- module_function
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
- File.exist?(entry_group_file_path)
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
- File.join(entries_folder, entries_file_name)
44
+ EntryGroupFileable.entry_group_file_path(time: time)
20
45
  end
21
46
 
22
47
  def entries_folder
23
- @entries_folder ||= configuration[:entries_folder]
48
+ @entries_folder ||= EntryGroupFileable.entries_folder
24
49
  end
25
50
 
26
51
  def entries_file_name
27
- @entries_file_name ||= time.strftime(configuration[: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] || Services::ConfigurationLoaderService.new.call
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
- module_function
11
-
12
- # returns a Hash having :time and :entries
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
- return JSON.parse(entry_group_json, symbolize_names: true).tap do |hash|
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
- private
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(entry_group_hash:, time:)
43
- time = entry_group_hash.fetch(:time, 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 = entry_group_hash.fetch(: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
- module_function
7
-
8
- def view_entry_group(time:)
9
- entry_group = if Models::EntryGroup.exists?(time: time)
10
- entry_group_json = Services::EntryGroupReaderService.new(time: time).call
11
- Services::EntryGroupHydratorService.new(entry_group_json: entry_group_json).call
12
- else
13
- Models::EntryGroup.new(time: time)
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 Dsu::Support::FieldErrors
10
+ include Support::FieldErrors
11
11
 
12
12
  def validate(record)
13
- raise 'options[:fields] is not defined.' unless options.key? :fields
14
- raise 'options[:fields] is not an Array.' unless options[:fields].is_a? Array
15
- raise 'options[:fields] elements are not Symbols.' unless options[:fields].all?(Symbol)
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
- options[:fields].each do |field|
18
- entries = record.send(field)
18
+ validate_entry_types record
19
+ validate_unique_entry record
20
+ validate_entries record
21
+ end
19
22
 
20
- unless entries.is_a?(Array)
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
- validate_entry_types field, entries, record
27
- validate_unique_entry_attr :uuid, field, entries, record
28
- validate_unique_entry_attr :description, field, entries, record
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
- private
35
+ def validate_unique_entry(record)
36
+ return unless record.entries.is_a? Array
33
37
 
34
- def validate_entry_types(field, entries, record)
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
- record.errors.add(field, 'entry Array element is the wrong object type. ' \
39
- "\"Entry\" was expected, but \"#{entry.class}\" was received.",
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
- next
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 validate_unique_entry_attr(attr, field, entries, record)
47
- return unless entries.is_a? Array
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
- attrs = entry_objects.map(&attr)
52
- return if attrs.uniq.length == attrs.length
55
+ entries.each do |entry|
56
+ next if entry.valid?
53
57
 
54
- non_unique_attrs = attrs.select { |attr| attrs.count(attr) > 1 }.uniq # rubocop:disable Lint/ShadowingOuterLocalVariable
55
- if non_unique_attrs.any?
56
- record.errors.add(field, "contains duplicate ##{attr}s: #{non_unique_attrs.join(', ')}.",
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
- raise 'options[:fields] is not defined.' unless options.key? :fields
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
- options[:fields].each do |field|
13
- time = record.send(field)
14
-
15
- if time.nil?
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
- if time.utc?
27
- record.errors.add(field, 'is not in localtime format.')
28
- next
29
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dsu
4
- VERSION = '0.1.0.alpha.5'
4
+ VERSION = '1.1.0.alpha.1'
5
5
  end
@@ -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