dsu 0.1.0.alpha.5 → 1.0.0
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 +11 -0
- data/Gemfile.lock +1 -1
- data/README.md +27 -40
- data/lib/dsu/base_cli.rb +0 -3
- data/lib/dsu/cli.rb +42 -49
- data/lib/dsu/command_services/add_entry_service.rb +21 -21
- data/lib/dsu/models/entry.rb +32 -20
- data/lib/dsu/models/entry_group.rb +43 -98
- data/lib/dsu/services/configuration_loader_service.rb +1 -0
- data/lib/dsu/services/entry_group_editor_service.rb +35 -84
- data/lib/dsu/services/stdout_redirector_service.rb +27 -0
- data/lib/dsu/support/colorable.rb +1 -0
- data/lib/dsu/support/entry_group_loadable.rb +10 -12
- 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 +30 -35
- data/lib/dsu/views/entry_group/show.rb +10 -4
- data/lib/dsu/views/shared/messages.rb +56 -0
- data/lib/dsu.rb +4 -0
- metadata +8 -8
- 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
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
3
|
+
require 'active_model'
|
4
4
|
require_relative '../services/entry_group_editor_service'
|
5
5
|
require_relative '../services/entry_group_deleter_service'
|
6
6
|
require_relative '../services/entry_group_reader_service'
|
@@ -13,30 +13,28 @@ require_relative 'entry'
|
|
13
13
|
|
14
14
|
module Dsu
|
15
15
|
module Models
|
16
|
-
class
|
16
|
+
# This class represents a group of entries for a given day. IOW,
|
17
|
+
# things someone might want to share at their daily standup (DSU).
|
18
|
+
class EntryGroup
|
19
|
+
include ActiveModel::Model
|
17
20
|
extend Support::EntryGroupLoadable
|
18
21
|
include Support::TimeFormatable
|
19
22
|
|
20
|
-
|
21
|
-
|
23
|
+
attr_accessor :time
|
24
|
+
attr_reader :entries
|
25
|
+
|
26
|
+
validates_with Validators::EntriesValidator
|
27
|
+
validates_with Validators::TimeValidator
|
22
28
|
|
23
29
|
def initialize(time: nil, entries: [])
|
24
30
|
raise ArgumentError, 'time is the wrong object type' unless time.is_a?(Time) || time.nil?
|
25
|
-
raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array) || entries.nil?
|
26
|
-
|
27
|
-
time ||= Time.now
|
28
|
-
time = time.localtime if time.utc?
|
29
31
|
|
30
|
-
|
31
|
-
|
32
|
-
super(hash: {
|
33
|
-
time: time,
|
34
|
-
entries: entries
|
35
|
-
})
|
32
|
+
@time = ensure_local_time(time)
|
33
|
+
self.entries = entries || []
|
36
34
|
end
|
37
35
|
|
38
36
|
class << self
|
39
|
-
def delete(time:, options: {})
|
37
|
+
def delete!(time:, options: {})
|
40
38
|
Services::EntryGroupDeleterService.new(time: time, options: options).call
|
41
39
|
end
|
42
40
|
|
@@ -46,7 +44,7 @@ module Dsu
|
|
46
44
|
# return new(time: time) unless exists?(time: time)
|
47
45
|
|
48
46
|
load(time: time).tap do |entry_group|
|
49
|
-
|
47
|
+
Services::EntryGroupEditorService.new(entry_group: entry_group, options: options).call
|
50
48
|
end
|
51
49
|
end
|
52
50
|
|
@@ -57,116 +55,63 @@ module Dsu
|
|
57
55
|
# Loads the EntryGroup from the file system and returns an
|
58
56
|
# instantiated EntryGroup object.
|
59
57
|
def load(time: nil)
|
60
|
-
|
58
|
+
load_entry_group_file_for(time: time)
|
61
59
|
end
|
62
60
|
|
63
61
|
# This function returns a hash whose :time and :entries
|
64
62
|
# key values are hydrated with instantiated Time and Entry
|
65
63
|
# objects.
|
66
|
-
def
|
67
|
-
|
68
|
-
hydrate_entry_group_hash(entry_group_hash: entry_group_hash, time: time)
|
64
|
+
def load_entry_group_for(time:)
|
65
|
+
load_entry_group_file_for(time: time)
|
69
66
|
end
|
67
|
+
end
|
70
68
|
|
71
|
-
|
72
|
-
|
73
|
-
end
|
69
|
+
def valid_unique_entries
|
70
|
+
entries&.select(&:valid?)&.uniq(&:description)
|
74
71
|
end
|
75
72
|
|
76
|
-
def
|
77
|
-
|
73
|
+
def clone
|
74
|
+
clone = super
|
75
|
+
|
76
|
+
clone.entries = clone.entries.map(&:clone)
|
77
|
+
clone
|
78
78
|
end
|
79
79
|
|
80
|
-
def
|
81
|
-
|
82
|
-
|
80
|
+
def entries=(entries)
|
81
|
+
entries ||= []
|
82
|
+
|
83
|
+
raise ArgumentError, 'entries is the wrong object type' unless entries.is_a?(Array)
|
84
|
+
raise ArgumentError, 'entries contains the wrong object type' unless entries.all?(Entry)
|
85
|
+
|
86
|
+
@entries = entries.map(&:clone)
|
83
87
|
end
|
84
88
|
|
85
89
|
# Deletes the entry group file from the file system.
|
86
|
-
def delete
|
87
|
-
self.class.delete(time: time)
|
90
|
+
def delete!
|
91
|
+
self.class.delete!(time: time)
|
92
|
+
self.entries = []
|
88
93
|
self
|
89
94
|
end
|
90
95
|
|
91
|
-
def entries?
|
92
|
-
entries.any?
|
93
|
-
end
|
94
|
-
|
95
96
|
def save!
|
97
|
+
delete! and return if entries.empty?
|
98
|
+
|
96
99
|
validate!
|
97
100
|
Services::EntryGroupWriterService.new(entry_group: self).call
|
101
|
+
self
|
98
102
|
end
|
99
103
|
|
100
104
|
def to_h
|
101
|
-
super.tap do |hash|
|
102
|
-
hash[:entries] = hash[:entries].dup
|
103
|
-
hash[:entries].each_with_index do |entry, index|
|
104
|
-
hash[:entries][index] = entry.to_h
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def check_unique(sha_or_editor_command:, description:)
|
110
|
-
raise ArgumentError, 'sha_or_editor_command is nil' if sha_or_editor_command.nil?
|
111
|
-
raise ArgumentError, 'description is nil' if description.nil?
|
112
|
-
raise ArgumentError, 'sha_or_editor_command is the wrong object type' unless sha_or_editor_command.is_a?(String)
|
113
|
-
raise ArgumentError, 'description is the wrong object type' unless description.is_a?(String)
|
114
|
-
|
115
|
-
if entries.blank?
|
116
|
-
entry_unique_hash = entry_unique_hash_for(uuid_unique: true, description_unique: true)
|
117
|
-
return entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
|
118
|
-
end
|
119
|
-
|
120
|
-
entry_hash = entries.each_with_object({}) do |entry_group_entry, hash|
|
121
|
-
hash[entry_group_entry.uuid] = entry_group_entry.description
|
122
|
-
end
|
123
|
-
|
124
|
-
# It is possible that sha_or_editor_command may have an editor command (e.g. +|a|add). If this
|
125
|
-
# is the case, just treat it as unique because when the entry is added, it will get a unique uuid.
|
126
|
-
uuid_unique = !sha_or_editor_command.match?(Entry::ENTRY_UUID_REGEX) || !entry_hash.key?(sha_or_editor_command)
|
127
|
-
entry_unique_hash = entry_unique_hash_for(
|
128
|
-
uuid: sha_or_editor_command,
|
129
|
-
uuid_unique: uuid_unique,
|
130
|
-
description: description,
|
131
|
-
description_unique: !entry_hash.value?(description)
|
132
|
-
)
|
133
|
-
entry_unique_struct_from(entry_unique_hash: entry_unique_hash)
|
134
|
-
end
|
135
|
-
|
136
|
-
def entry_unique_hash_for(uuid_unique:, description_unique:, uuid: nil, description: nil)
|
137
105
|
{
|
138
|
-
|
139
|
-
|
140
|
-
description: description,
|
141
|
-
description_unique: description_unique,
|
142
|
-
formatted_time: Support::TimeFormatable.formatted_time(time: time)
|
106
|
+
time: time.dup,
|
107
|
+
entries: entries.map(&:to_h)
|
143
108
|
}
|
144
109
|
end
|
145
110
|
|
146
|
-
|
147
|
-
Struct.new(*entry_unique_hash.keys, keyword_init: true) do
|
148
|
-
def unique?
|
149
|
-
uuid_unique? && description_unique?
|
150
|
-
end
|
151
|
-
|
152
|
-
def uuid_unique?
|
153
|
-
uuid_unique
|
154
|
-
end
|
155
|
-
|
156
|
-
def description_unique?
|
157
|
-
description_unique
|
158
|
-
end
|
159
|
-
|
160
|
-
def messages
|
161
|
-
return [] if unique?
|
111
|
+
private
|
162
112
|
|
163
|
-
|
164
|
-
|
165
|
-
messages = []
|
166
|
-
messages << "#uuid is not unique: \"#{uuid} #{short_description}\"" unless uuid_unique?
|
167
|
-
messages << "#description is not unique: \"#{uuid} #{short_description}\""
|
168
|
-
end
|
169
|
-
end.new(**entry_unique_hash)
|
113
|
+
def ensure_local_time(time)
|
114
|
+
time.nil? ? Time.now : time.dup.localtime
|
170
115
|
end
|
171
116
|
end
|
172
117
|
end
|
@@ -4,7 +4,10 @@ require_relative '../models/entry'
|
|
4
4
|
require_relative '../support/colorable'
|
5
5
|
require_relative '../support/say'
|
6
6
|
require_relative '../support/time_formatable'
|
7
|
+
require_relative '../views/edited_entries/shared/errors'
|
8
|
+
require_relative '../views/shared/messages'
|
7
9
|
require_relative 'configuration_loader_service'
|
10
|
+
require_relative 'stdout_redirector_service'
|
8
11
|
|
9
12
|
module Dsu
|
10
13
|
module Services
|
@@ -24,7 +27,7 @@ module Dsu
|
|
24
27
|
|
25
28
|
def call
|
26
29
|
edit_view = render_edit_view
|
27
|
-
edit
|
30
|
+
edit edit_view
|
28
31
|
# NOTE: Return the original entry group object as any permanent changes
|
29
32
|
# will have been applied to it.
|
30
33
|
entry_group
|
@@ -38,106 +41,54 @@ module Dsu
|
|
38
41
|
# and edit it. The edits will be used to update the entry group.
|
39
42
|
def render_edit_view
|
40
43
|
say "Editing entry group #{formatted_time(time: entry_group.time)}...", HIGHLIGHT
|
41
|
-
|
44
|
+
StdoutRedirectorService.call { Views::EntryGroup::Edit.new(entry_group: entry_group).render }
|
42
45
|
end
|
43
46
|
|
44
|
-
# Writes the temporary file contents to disk and opens it in the editor
|
45
|
-
|
47
|
+
# Writes the temporary file contents to disk and opens it in the editor
|
48
|
+
# for editing. It then copies the changes to the entry group and writes
|
49
|
+
# the changes to the entry group file.
|
50
|
+
def edit(edit_view)
|
51
|
+
entry_group_with_edits = Models::EntryGroup.new(time: entry_group.time)
|
52
|
+
|
46
53
|
Services::TempFileWriterService.new(tmp_file_content: edit_view).call do |tmp_file_path|
|
47
|
-
|
48
|
-
|
54
|
+
if Kernel.system("${EDITOR:-#{configuration[:editor]}} #{tmp_file_path}")
|
55
|
+
Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |editor_line|
|
56
|
+
next unless process_description?(editor_line)
|
57
|
+
|
58
|
+
entry_group_with_edits.entries << Models::Entry.new(description: editor_line)
|
59
|
+
end
|
60
|
+
|
61
|
+
process_entry_group!(entry_group_with_edits)
|
62
|
+
else
|
63
|
+
say "Failed to open temporary file in editor '#{configuration[:editor]}'; " \
|
49
64
|
"the system error returned was: '#{$CHILD_STATUS}'.", ERROR
|
50
65
|
say 'Either set the EDITOR environment variable ' \
|
51
66
|
'or set the dsu editor configuration option (`$ dsu config init`).', ERROR
|
52
|
-
say 'Run `$ dsu help config` for more information
|
53
|
-
|
54
|
-
system('dsu help config')
|
55
|
-
|
56
|
-
return # rubocop:disable Lint/NonLocalExitFromIterator: This is not an iterator.
|
67
|
+
say 'Run `$ dsu help config` for more information:', ERROR
|
57
68
|
end
|
58
|
-
|
59
|
-
update_entry_group!(tmp_file_path: tmp_file_path)
|
60
69
|
end
|
61
70
|
end
|
62
71
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
Services::TempFileReaderService.new(tmp_file_path: tmp_file_path).call do |tmp_file_line|
|
68
|
-
next if comment_or_empty?(tmp_file_line: tmp_file_line)
|
69
|
-
|
70
|
-
entry_info = editor_entry_info_from(tmp_file_line: tmp_file_line)
|
71
|
-
next if entry_info.empty?
|
72
|
-
next if delete_entry_cmd?(sha: entry_info[:sha])
|
73
|
-
next unless add_entry_cmd?(sha: entry_info[:sha]) || sha?(sha: entry_info[:sha])
|
74
|
-
|
75
|
-
entry_info[:sha_or_editor_command] = entry_info[:sha]
|
76
|
-
entry_info[:sha] = nil if add_entry_cmd?(sha: entry_info[:sha])
|
77
|
-
|
78
|
-
entry = Models::Entry.new(uuid: entry_info[:sha], description: entry_info[:description])
|
79
|
-
entry_group.check_unique(sha_or_editor_command: entry_info[:sha_or_editor_command],
|
80
|
-
description: entry_info[:description]).tap do |status|
|
81
|
-
entries << entry and next if status.unique?
|
82
|
-
|
83
|
-
errors << status.messages
|
84
|
-
end
|
72
|
+
def process_entry_group!(entry_group_with_edits)
|
73
|
+
if entry_group_with_edits.entries.empty?
|
74
|
+
entry_group.delete!
|
75
|
+
return
|
85
76
|
end
|
86
77
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
78
|
+
if entry_group_with_edits.invalid?
|
79
|
+
header = 'The following ERRORS were encountered; these changes were not saved:'
|
80
|
+
messages = entry_group_with_edits.errors.full_messages
|
81
|
+
Views::Shared::Messages.new(messages: messages, message_type: :error, options: { header: header }).render
|
91
82
|
end
|
92
83
|
|
93
|
-
#
|
94
|
-
entry_group.entries =
|
95
|
-
entry_group.delete and return unless entry_group.entries?
|
96
|
-
|
84
|
+
# Make sure we're saving only valid, unique entries.
|
85
|
+
entry_group.entries = entry_group_with_edits.valid_unique_entries
|
97
86
|
entry_group.save!
|
98
87
|
end
|
99
88
|
|
100
|
-
def
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
def delete_entry_cmd?(sha:)
|
105
|
-
%w[- d delete].include?(sha)
|
106
|
-
end
|
107
|
-
|
108
|
-
def add_entry_cmd?(sha:)
|
109
|
-
%w[+ a add].include?(sha)
|
110
|
-
end
|
111
|
-
|
112
|
-
def comment_or_empty?(tmp_file_line:)
|
113
|
-
['#', nil].include? tmp_file_line[0]
|
114
|
-
end
|
115
|
-
|
116
|
-
def editor_entry_info_from(tmp_file_line:)
|
117
|
-
match_data = tmp_file_line.match(/(\S+)\s(.+)/)
|
118
|
-
{
|
119
|
-
sha: match_data[1]&.strip,
|
120
|
-
description: match_data[2]&.strip
|
121
|
-
}
|
122
|
-
rescue StandardError
|
123
|
-
{}
|
124
|
-
end
|
125
|
-
|
126
|
-
# TODO: Add this to a module.
|
127
|
-
# https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
|
128
|
-
def capture_stdxxx
|
129
|
-
# The output stream must be an IO-like object. In this case we capture it in
|
130
|
-
# an in-memory IO object so we can return the string value. You can assign any
|
131
|
-
# IO object here.
|
132
|
-
string_io = StringIO.new
|
133
|
-
prev_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
|
134
|
-
prev_stderr, $stderr = $stderr, string_io # rubocop:disable Style/ParallelAssignment
|
135
|
-
yield
|
136
|
-
string_io.string
|
137
|
-
ensure
|
138
|
-
# Restore the previous value of stderr and stdout (typically equal to STDERR).
|
139
|
-
$stdout = prev_stdout
|
140
|
-
$stderr = prev_stderr
|
89
|
+
def process_description?(description)
|
90
|
+
description = Models::Entry.clean_description(description)
|
91
|
+
!(description.blank? || description[0] == '#')
|
141
92
|
end
|
142
93
|
|
143
94
|
def configuration
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dsu
|
4
|
+
module Services
|
5
|
+
# This service captures $stdout, resirects it to a StringIO object,
|
6
|
+
# and returns the string value.
|
7
|
+
# https://stackoverflow.com/questions/4459330/how-do-i-temporarily-redirect-stderr-in-ruby/4459463#4459463
|
8
|
+
module StdoutRedirectorService
|
9
|
+
class << self
|
10
|
+
def call
|
11
|
+
raise ArgumentError, 'no block was provided' unless block_given?
|
12
|
+
|
13
|
+
# The output stream must be an IO-like object. In this case we capture it in
|
14
|
+
# an in-memory IO object so we can return the string value. Any IO object can
|
15
|
+
# be used here.
|
16
|
+
string_io = StringIO.new
|
17
|
+
original_stdout, $stdout = $stdout, string_io # rubocop:disable Style/ParallelAssignment
|
18
|
+
yield
|
19
|
+
string_io.string
|
20
|
+
ensure
|
21
|
+
# Restore the original $stdout.
|
22
|
+
$stdout = original_stdout
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -3,6 +3,7 @@
|
|
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
|
@@ -12,22 +13,19 @@ module Dsu
|
|
12
13
|
# returns a Hash having :time and :entries
|
13
14
|
# where entries == an Array of Entry Hashes
|
14
15
|
# representing the JSON Entry objects for :time.
|
15
|
-
def
|
16
|
+
def load_entry_group_file_for(time:)
|
16
17
|
entry_group_json = Services::EntryGroupReaderService.new(time: time).call
|
17
|
-
if entry_group_json.present?
|
18
|
-
|
18
|
+
hash = if entry_group_json.present?
|
19
|
+
JSON.parse(entry_group_json, symbolize_names: true).tap do |hash|
|
19
20
|
hash[:time] = Time.parse(hash[:time])
|
20
21
|
end
|
22
|
+
else
|
23
|
+
{ time: time, entries: [] }
|
21
24
|
end
|
22
25
|
|
23
|
-
|
24
|
-
time: time,
|
25
|
-
entries: []
|
26
|
-
}
|
26
|
+
Models::EntryGroup.new(**hydrate_entry_group_hash(hash: hash, time: time))
|
27
27
|
end
|
28
28
|
|
29
|
-
private
|
30
|
-
|
31
29
|
# Accepts an entry group hash and returns a
|
32
30
|
# hydrated entry group hash:
|
33
31
|
#
|
@@ -39,10 +37,10 @@ module Dsu
|
|
39
37
|
# ...
|
40
38
|
# ]
|
41
39
|
# }
|
42
|
-
def hydrate_entry_group_hash(
|
43
|
-
time =
|
40
|
+
def hydrate_entry_group_hash(hash:, time:)
|
41
|
+
time = hash.fetch(:time, time)
|
44
42
|
time = Time.parse(time) unless time.is_a? Time
|
45
|
-
entries =
|
43
|
+
entries = hash.fetch(:entries, [])
|
46
44
|
entries = entries.map { |entry_hash| Models::Entry.new(**entry_hash) }
|
47
45
|
|
48
46
|
{ time: time, entries: entries }
|
@@ -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