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.
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