dsu 0.1.0.alpha.5 → 1.0.0
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 +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