cequel 0.5.6 → 1.0.0.pre.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.
- checksums.yaml +7 -0
- data/lib/cequel.rb +5 -8
- data/lib/cequel/errors.rb +1 -0
- data/lib/cequel/metal.rb +17 -0
- data/lib/cequel/metal/batch.rb +62 -0
- data/lib/cequel/metal/cql_row_specification.rb +26 -0
- data/lib/cequel/metal/data_set.rb +461 -0
- data/lib/cequel/metal/deleter.rb +47 -0
- data/lib/cequel/metal/incrementer.rb +35 -0
- data/lib/cequel/metal/inserter.rb +53 -0
- data/lib/cequel/metal/keyspace.rb +213 -0
- data/lib/cequel/metal/row.rb +48 -0
- data/lib/cequel/metal/row_specification.rb +37 -0
- data/lib/cequel/metal/statement.rb +30 -0
- data/lib/cequel/metal/updater.rb +65 -0
- data/lib/cequel/metal/writer.rb +73 -0
- data/lib/cequel/model.rb +12 -84
- data/lib/cequel/model/association_collection.rb +23 -0
- data/lib/cequel/model/associations.rb +84 -80
- data/lib/cequel/model/base.rb +74 -0
- data/lib/cequel/model/belongs_to_association.rb +31 -0
- data/lib/cequel/model/callbacks.rb +14 -10
- data/lib/cequel/model/collection.rb +255 -0
- data/lib/cequel/model/errors.rb +6 -6
- data/lib/cequel/model/has_many_association.rb +26 -0
- data/lib/cequel/model/mass_assignment.rb +31 -0
- data/lib/cequel/model/persistence.rb +119 -115
- data/lib/cequel/model/properties.rb +89 -87
- data/lib/cequel/model/railtie.rb +21 -14
- data/lib/cequel/model/record_set.rb +285 -0
- data/lib/cequel/model/schema.rb +33 -0
- data/lib/cequel/model/scoped.rb +5 -48
- data/lib/cequel/model/validations.rb +18 -18
- data/lib/cequel/schema.rb +15 -0
- data/lib/cequel/schema/column.rb +135 -0
- data/lib/cequel/schema/create_table_dsl.rb +56 -0
- data/lib/cequel/schema/keyspace.rb +50 -0
- data/lib/cequel/schema/table.rb +120 -0
- data/lib/cequel/schema/table_property.rb +67 -0
- data/lib/cequel/schema/table_reader.rb +139 -0
- data/lib/cequel/schema/table_synchronizer.rb +114 -0
- data/lib/cequel/schema/table_updater.rb +83 -0
- data/lib/cequel/schema/table_writer.rb +80 -0
- data/lib/cequel/schema/update_table_dsl.rb +60 -0
- data/lib/cequel/type.rb +232 -0
- data/lib/cequel/version.rb +1 -1
- data/spec/environment.rb +5 -1
- data/spec/examples/metal/data_set_spec.rb +608 -0
- data/spec/examples/model/associations_spec.rb +84 -74
- data/spec/examples/model/callbacks_spec.rb +66 -59
- data/spec/examples/model/list_spec.rb +393 -0
- data/spec/examples/model/map_spec.rb +229 -0
- data/spec/examples/model/mass_assignment_spec.rb +55 -0
- data/spec/examples/model/naming_spec.rb +11 -4
- data/spec/examples/model/persistence_spec.rb +140 -150
- data/spec/examples/model/properties_spec.rb +122 -75
- data/spec/examples/model/record_set_spec.rb +285 -0
- data/spec/examples/model/schema_spec.rb +44 -0
- data/spec/examples/model/serialization_spec.rb +20 -14
- data/spec/examples/model/set_spec.rb +133 -0
- data/spec/examples/model/spec_helper.rb +0 -10
- data/spec/examples/model/validations_spec.rb +51 -38
- data/spec/examples/schema/table_reader_spec.rb +328 -0
- data/spec/examples/schema/table_synchronizer_spec.rb +172 -0
- data/spec/examples/schema/table_updater_spec.rb +157 -0
- data/spec/examples/schema/table_writer_spec.rb +225 -0
- data/spec/examples/spec_helper.rb +29 -0
- data/spec/examples/type_spec.rb +204 -0
- data/spec/support/helpers.rb +67 -8
- metadata +121 -152
- data/lib/cequel/batch.rb +0 -58
- data/lib/cequel/cql_row_specification.rb +0 -22
- data/lib/cequel/data_set.rb +0 -371
- data/lib/cequel/keyspace.rb +0 -205
- data/lib/cequel/model/class_internals.rb +0 -49
- data/lib/cequel/model/column.rb +0 -20
- data/lib/cequel/model/counter.rb +0 -35
- data/lib/cequel/model/dictionary.rb +0 -126
- data/lib/cequel/model/dirty.rb +0 -53
- data/lib/cequel/model/dynamic.rb +0 -31
- data/lib/cequel/model/inheritable.rb +0 -48
- data/lib/cequel/model/instance_internals.rb +0 -23
- data/lib/cequel/model/local_association.rb +0 -42
- data/lib/cequel/model/magic.rb +0 -79
- data/lib/cequel/model/mass_assignment_security.rb +0 -21
- data/lib/cequel/model/naming.rb +0 -17
- data/lib/cequel/model/observer.rb +0 -42
- data/lib/cequel/model/readable_dictionary.rb +0 -182
- data/lib/cequel/model/remote_association.rb +0 -40
- data/lib/cequel/model/scope.rb +0 -362
- data/lib/cequel/model/subclass_internals.rb +0 -45
- data/lib/cequel/model/timestamps.rb +0 -52
- data/lib/cequel/model/translation.rb +0 -17
- data/lib/cequel/row_specification.rb +0 -63
- data/lib/cequel/statement.rb +0 -23
- data/spec/examples/data_set_spec.rb +0 -444
- data/spec/examples/keyspace_spec.rb +0 -84
- data/spec/examples/model/counter_spec.rb +0 -94
- data/spec/examples/model/dictionary_spec.rb +0 -301
- data/spec/examples/model/dirty_spec.rb +0 -39
- data/spec/examples/model/dynamic_spec.rb +0 -41
- data/spec/examples/model/inheritable_spec.rb +0 -45
- data/spec/examples/model/magic_spec.rb +0 -199
- data/spec/examples/model/mass_assignment_security_spec.rb +0 -13
- data/spec/examples/model/observer_spec.rb +0 -86
- data/spec/examples/model/scope_spec.rb +0 -677
- data/spec/examples/model/timestamps_spec.rb +0 -52
- data/spec/examples/model/translation_spec.rb +0 -23
data/lib/cequel/model/railtie.rb
CHANGED
@@ -4,27 +4,34 @@ module Cequel
|
|
4
4
|
|
5
5
|
class Railtie < Rails::Railtie
|
6
6
|
|
7
|
-
config.cequel = Cequel::Model
|
7
|
+
config.cequel = Cequel::Model::Base
|
8
8
|
|
9
9
|
initializer "cequel.configure_rails" do
|
10
|
+
app_name = Rails.application.railtie_name.sub(/_application$/, '')
|
10
11
|
config_path = Rails.root.join('config/cequel.yml').to_s
|
11
12
|
|
12
13
|
if File.exist?(config_path)
|
13
|
-
|
14
|
-
|
14
|
+
config = YAML::load(ERB.new(IO.read(config_path)).result)[Rails.env].
|
15
|
+
deep_symbolize_keys
|
16
|
+
else
|
17
|
+
config = {host: '127.0.0.1:9160'}
|
15
18
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
19
|
+
config.reverse_merge!(keyspace: "#{app_name}_#{Rails.env}")
|
20
|
+
connection = Cequel.connect(config)
|
21
|
+
|
22
|
+
begin
|
23
|
+
connection = Cequel.connect(config)
|
24
|
+
rescue CassandraCQL::Error::InvalidRequestException
|
25
|
+
connection = Cequel.connect(config.except(:keyspace))
|
26
|
+
#XXX This should be read from the configuration
|
27
|
+
connection.execute(<<-CQL)
|
28
|
+
CREATE KEYSPACE #{keyspace}
|
29
|
+
WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1}
|
30
|
+
CQL
|
31
|
+
retry
|
27
32
|
end
|
33
|
+
connection.logger = Rails.logger
|
34
|
+
Cequel::Model::Base.connection = connection
|
28
35
|
end
|
29
36
|
end
|
30
37
|
|
@@ -0,0 +1,285 @@
|
|
1
|
+
module Cequel
|
2
|
+
|
3
|
+
module Model
|
4
|
+
|
5
|
+
class RecordSet
|
6
|
+
|
7
|
+
extend Forwardable
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
Bound = Struct.new(:value, :inclusive)
|
11
|
+
|
12
|
+
def initialize(clazz)
|
13
|
+
@clazz = clazz
|
14
|
+
@select_columns = []
|
15
|
+
@scoped_key_values = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def all
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def select(*columns)
|
23
|
+
return super if block_given?
|
24
|
+
scoped { |record_set| record_set.select_columns.concat(columns) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def limit(count)
|
28
|
+
scoped { |record_set| record_set.row_limit = count }
|
29
|
+
end
|
30
|
+
|
31
|
+
def at(*scoped_key_values)
|
32
|
+
record_set_class = next_key_column.partition_key? ?
|
33
|
+
RecordSet : SortableRecordSet
|
34
|
+
scoped(record_set_class) do |record_set|
|
35
|
+
record_set.scoped_key_values.concat(scoped_key_values)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](scoped_key_value)
|
40
|
+
if next_key_column
|
41
|
+
at(scoped_key_value)
|
42
|
+
else
|
43
|
+
attributes = {}
|
44
|
+
key_values = [*scoped_key_values, scoped_key_value]
|
45
|
+
clazz.key_column_names.zip(key_values) do |key_name, key_value|
|
46
|
+
attributes[key_name] = key_value
|
47
|
+
end
|
48
|
+
clazz.new_empty { @attributes = attributes }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def find(*scoped_key_values)
|
53
|
+
self[*scoped_key_values].load!
|
54
|
+
end
|
55
|
+
|
56
|
+
def /(scoped_key_value)
|
57
|
+
at(scoped_key_value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def after(start_key)
|
61
|
+
scoped do |record_set|
|
62
|
+
record_set.lower_bound = Bound.new(start_key, false)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def before(end_key)
|
67
|
+
scoped do |record_set|
|
68
|
+
record_set.upper_bound = Bound.new(end_key, false)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def in(range)
|
73
|
+
scoped do |record_set|
|
74
|
+
record_set.lower_bound = Bound.new(range.first, true)
|
75
|
+
record_set.upper_bound = Bound.new(range.last, !range.exclude_end?)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def first(count = nil)
|
80
|
+
count ? limit(count).entries : limit(1).each.first
|
81
|
+
end
|
82
|
+
|
83
|
+
def count
|
84
|
+
data_set.count
|
85
|
+
end
|
86
|
+
|
87
|
+
def each(&block)
|
88
|
+
find_each(&block)
|
89
|
+
end
|
90
|
+
|
91
|
+
def find_each(options = {})
|
92
|
+
return enum_for(:find_each, options) unless block_given?
|
93
|
+
find_each_row(options) { |row| yield clazz.hydrate(row) }
|
94
|
+
end
|
95
|
+
|
96
|
+
def find_each_row(options = {}, &block)
|
97
|
+
return enum_for(:find_each_row, options) unless block
|
98
|
+
find_rows_in_batches(options) { |row| row.each(&block) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def find_rows_in_batches(options = {}, &block)
|
102
|
+
return find_rows_in_single_batch(options, &block) if row_limit
|
103
|
+
batch_size = options.fetch(:batch_size, 1000)
|
104
|
+
batch_record_set = base_record_set = limit(batch_size)
|
105
|
+
more_results = true
|
106
|
+
|
107
|
+
while more_results
|
108
|
+
rows = batch_record_set.find_rows_in_single_batch
|
109
|
+
yield rows if rows.any?
|
110
|
+
more_results = rows.length == batch_size
|
111
|
+
last_row = rows.last
|
112
|
+
if more_results
|
113
|
+
find_nested_batches_from(last_row, options, &block)
|
114
|
+
batch_record_set = base_record_set.next_batch_from(last_row)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
protected
|
120
|
+
attr_accessor :row_limit
|
121
|
+
attr_reader :select_columns, :scoped_key_values,
|
122
|
+
:lower_bound, :upper_bound
|
123
|
+
|
124
|
+
def reversed?
|
125
|
+
false
|
126
|
+
end
|
127
|
+
|
128
|
+
def lower_bound=(bound)
|
129
|
+
@lower_bound = bound
|
130
|
+
end
|
131
|
+
|
132
|
+
def upper_bound=(bound)
|
133
|
+
@upper_bound = bound
|
134
|
+
end
|
135
|
+
|
136
|
+
def data_set
|
137
|
+
@data_set ||= construct_data_set
|
138
|
+
end
|
139
|
+
|
140
|
+
def next_batch_from(row)
|
141
|
+
reversed? ? before(row[range_key_name]) : after(row[range_key_name])
|
142
|
+
end
|
143
|
+
|
144
|
+
def find_nested_batches_from(row, options, &block)
|
145
|
+
if next_key_column
|
146
|
+
at(row[range_key_name]).
|
147
|
+
next_batch_from(row).
|
148
|
+
find_rows_in_batches(options, &block)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def find_rows_in_single_batch(options = {})
|
153
|
+
if options.key?(:batch_size)
|
154
|
+
raise ArgumentError,
|
155
|
+
"Can't pass :batch_size argument with a limit in the scope"
|
156
|
+
else
|
157
|
+
data_set.entries.tap do |batch|
|
158
|
+
yield batch if batch.any? && block_given?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def range_key
|
164
|
+
clazz.key_columns[scoped_key_values.length]
|
165
|
+
end
|
166
|
+
|
167
|
+
def range_key_name
|
168
|
+
range_key.name
|
169
|
+
end
|
170
|
+
|
171
|
+
def scoped_key_columns
|
172
|
+
clazz.key_columns.first(scoped_key_values.length)
|
173
|
+
end
|
174
|
+
|
175
|
+
def scoped_key_names
|
176
|
+
scoped_key_columns.map { |column| column.name }
|
177
|
+
end
|
178
|
+
|
179
|
+
def chain_from(collection)
|
180
|
+
@select_columns = collection.select_columns.dup
|
181
|
+
@scoped_key_values = collection.scoped_key_values.dup
|
182
|
+
@lower_bound = collection.lower_bound
|
183
|
+
@upper_bound = collection.upper_bound
|
184
|
+
@row_limit = collection.row_limit
|
185
|
+
self
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
attr_reader :clazz
|
190
|
+
def_delegators :clazz, :connection
|
191
|
+
|
192
|
+
def scoped(record_set_class = self.class, &block)
|
193
|
+
record_set_class.new(clazz).chain_from(self).tap(&block)
|
194
|
+
end
|
195
|
+
|
196
|
+
def next_key_column
|
197
|
+
clazz.key_columns[scoped_key_values.length + 1]
|
198
|
+
end
|
199
|
+
|
200
|
+
def next_key_name
|
201
|
+
next_key_column.name if next_key_column
|
202
|
+
end
|
203
|
+
|
204
|
+
def construct_data_set
|
205
|
+
data_set = connection[clazz.table_name]
|
206
|
+
data_set = data_set.limit(row_limit) if row_limit
|
207
|
+
data_set = data_set.select(*select_columns) if select_columns
|
208
|
+
if scoped_key_values
|
209
|
+
key_conditions = Hash[scoped_key_names.zip(scoped_key_values)]
|
210
|
+
data_set = data_set.where(key_conditions)
|
211
|
+
end
|
212
|
+
if lower_bound
|
213
|
+
fragment = construct_bound_fragment(lower_bound, '>')
|
214
|
+
data_set = data_set.where(fragment, lower_bound.value)
|
215
|
+
end
|
216
|
+
if upper_bound
|
217
|
+
fragment = construct_bound_fragment(upper_bound, '<')
|
218
|
+
data_set = data_set.where(fragment, upper_bound.value)
|
219
|
+
end
|
220
|
+
data_set
|
221
|
+
end
|
222
|
+
|
223
|
+
def construct_bound_fragment(bound, base_operator)
|
224
|
+
operator = bound.inclusive ? "#{base_operator}=" : base_operator
|
225
|
+
"TOKEN(#{range_key_name}) #{operator} TOKEN(?)"
|
226
|
+
end
|
227
|
+
|
228
|
+
end
|
229
|
+
|
230
|
+
class SortableRecordSet < RecordSet
|
231
|
+
|
232
|
+
def initialize(clazz)
|
233
|
+
super
|
234
|
+
@reversed = false
|
235
|
+
end
|
236
|
+
|
237
|
+
def from(start_key)
|
238
|
+
scoped do |record_set|
|
239
|
+
record_set.lower_bound = Bound.new(start_key, true)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def upto(end_key)
|
244
|
+
scoped do |record_set|
|
245
|
+
record_set.upper_bound = Bound.new(end_key, true)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def reverse
|
250
|
+
scoped { |scope| scope.reversed = !reversed? }
|
251
|
+
end
|
252
|
+
|
253
|
+
def last
|
254
|
+
reverse.first
|
255
|
+
end
|
256
|
+
|
257
|
+
def chain_from(collection)
|
258
|
+
super
|
259
|
+
@reversed = collection.reversed?
|
260
|
+
self
|
261
|
+
end
|
262
|
+
|
263
|
+
protected
|
264
|
+
attr_writer :reversed
|
265
|
+
|
266
|
+
def construct_data_set
|
267
|
+
data_set = super
|
268
|
+
data_set = data_set.order(range_key_name => :desc) if reversed?
|
269
|
+
data_set
|
270
|
+
end
|
271
|
+
|
272
|
+
def reversed?
|
273
|
+
@reversed
|
274
|
+
end
|
275
|
+
|
276
|
+
def construct_bound_fragment(bound, base_operator)
|
277
|
+
operator = bound.inclusive ? "#{base_operator}=" : base_operator
|
278
|
+
"#{range_key_name} #{operator} ?"
|
279
|
+
end
|
280
|
+
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Cequel
|
2
|
+
|
3
|
+
module Model
|
4
|
+
|
5
|
+
module Schema
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
def_delegators :table_schema, :key_columns, :key_column_names
|
13
|
+
|
14
|
+
def synchronize_schema
|
15
|
+
Cequel::Schema::TableSynchronizer.
|
16
|
+
apply(connection, read_schema, table_schema)
|
17
|
+
end
|
18
|
+
|
19
|
+
def read_schema
|
20
|
+
connection.schema.read_table(table_name)
|
21
|
+
end
|
22
|
+
|
23
|
+
def table_schema
|
24
|
+
@table_schema ||= Cequel::Schema::Table.new(table_name)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/lib/cequel/model/scoped.rb
CHANGED
@@ -4,55 +4,12 @@ module Cequel
|
|
4
4
|
|
5
5
|
module Scoped
|
6
6
|
|
7
|
-
extend
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
delegate :consistency, :count, :first, :limit, :select, :where,
|
11
|
-
:find_in_batches, :find_each, :find_rows_in_batches, :find_each_row,
|
12
|
-
:to => :all
|
13
|
-
|
14
|
-
def default_scope(scope)
|
15
|
-
@_cequel.default_scope = scope
|
16
|
-
end
|
17
|
-
|
18
|
-
def all
|
19
|
-
current_scope || @_cequel.default_scope || empty_scope
|
20
|
-
end
|
21
|
-
|
22
|
-
def select(*rows)
|
23
|
-
all.select(*rows)
|
24
|
-
end
|
25
|
-
|
26
|
-
def with_scope(scope)
|
27
|
-
@_cequel.synchronize do
|
28
|
-
old_scope = current_scope
|
29
|
-
begin
|
30
|
-
self.current_scope = scope
|
31
|
-
yield
|
32
|
-
ensure
|
33
|
-
self.current_scope = old_scope
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
private
|
39
|
-
|
40
|
-
def empty_scope
|
41
|
-
Scope.new(self, [column_family])
|
42
|
-
end
|
43
|
-
|
44
|
-
def current_scope
|
45
|
-
::Thread.current[current_scope_key]
|
46
|
-
end
|
47
|
-
|
48
|
-
def current_scope=(scope)
|
49
|
-
::Thread.current[current_scope_key] = scope
|
50
|
-
end
|
51
|
-
|
52
|
-
def current_scope_key
|
53
|
-
:"cequel-current_scope-#{object_id}"
|
54
|
-
end
|
7
|
+
extend Forwardable
|
55
8
|
|
9
|
+
def_delegators :current_scope, *RecordSet.instance_methods(false)
|
10
|
+
|
11
|
+
def current_scope
|
12
|
+
RecordSet.new(self)
|
56
13
|
end
|
57
14
|
|
58
15
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Cequel
|
2
|
-
|
2
|
+
|
3
3
|
module Model
|
4
4
|
|
5
5
|
module Validations
|
@@ -8,35 +8,35 @@ module Cequel
|
|
8
8
|
|
9
9
|
included do
|
10
10
|
include ActiveModel::Validations
|
11
|
-
|
11
|
+
define_model_callbacks :validation
|
12
|
+
alias_method_chain :valid?, :callbacks
|
12
13
|
end
|
13
14
|
|
14
15
|
module ClassMethods
|
15
16
|
|
16
|
-
def create!(attributes
|
17
|
-
|
18
|
-
instance.save!
|
17
|
+
def create!(attributes, &block)
|
18
|
+
new(attributes, &block).save!
|
19
19
|
end
|
20
20
|
|
21
21
|
end
|
22
22
|
|
23
|
-
def save(
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
else
|
28
|
-
false
|
29
|
-
end
|
23
|
+
def save(options = {})
|
24
|
+
validate = options.fetch(:validate, true)
|
25
|
+
options.delete(:validate)
|
26
|
+
(!validate || valid?) && super
|
30
27
|
end
|
31
28
|
|
32
|
-
def save!(
|
33
|
-
|
34
|
-
|
29
|
+
def save!(options = {})
|
30
|
+
tap do
|
31
|
+
unless save(options)
|
32
|
+
raise RecordInvalid, errors.full_messages.join("; ")
|
33
|
+
end
|
34
|
+
end
|
35
35
|
end
|
36
36
|
|
37
|
-
def update_attributes!(
|
38
|
-
|
39
|
-
|
37
|
+
def update_attributes!(attributes)
|
38
|
+
self.attributes = attributes
|
39
|
+
save!
|
40
40
|
end
|
41
41
|
|
42
42
|
def valid_with_callbacks?
|