activerecord 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (35) hide show
  1. data/CHANGELOG +65 -0
  2. data/install.rb +2 -2
  3. data/lib/active_record.rb +1 -1
  4. data/lib/active_record/acts/list.rb +13 -13
  5. data/lib/active_record/associations.rb +38 -0
  6. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -1
  7. data/lib/active_record/associations/has_many_association.rb +2 -1
  8. data/lib/active_record/base.rb +44 -20
  9. data/lib/active_record/connection_adapters/abstract_adapter.rb +10 -11
  10. data/lib/active_record/connection_adapters/mysql_adapter.rb +6 -2
  11. data/lib/active_record/connection_adapters/postgresql_adapter.rb +7 -2
  12. data/lib/active_record/connection_adapters/sqlite_adapter.rb +10 -2
  13. data/lib/active_record/fixtures.rb +18 -0
  14. data/lib/active_record/support/binding_of_caller.rb +81 -0
  15. data/lib/active_record/support/breakpoint.rb +525 -0
  16. data/lib/active_record/timestamp.rb +17 -5
  17. data/lib/active_record/transactions.rb +11 -13
  18. data/lib/active_record/validations.rb +31 -13
  19. data/rakefile +1 -1
  20. data/test/abstract_unit.rb +2 -0
  21. data/test/base_test.rb +29 -9
  22. data/test/fixtures/db_definitions/mysql.sql +5 -3
  23. data/test/fixtures/db_definitions/postgresql.sql +2 -0
  24. data/test/fixtures/db_definitions/sqlite.sql +3 -1
  25. data/test/fixtures/db_definitions/sqlserver.sql +2 -0
  26. data/test/fixtures/developer.rb +2 -4
  27. data/test/fixtures/developers.yml +3 -0
  28. data/test/fixtures/fixture_database.sqlite +0 -0
  29. data/test/fixtures/fixture_database_2.sqlite +0 -0
  30. data/test/fixtures/mixin.rb +5 -2
  31. data/test/fixtures/mixins.yml +27 -11
  32. data/test/mixin_test.rb +154 -56
  33. data/test/transactions_test.rb +1 -1
  34. data/test/validations_test.rb +50 -3
  35. metadata +4 -2
@@ -7,19 +7,31 @@ module ActiveRecord
7
7
  module Timestamp
8
8
  def self.append_features(base) # :nodoc:
9
9
  super
10
- base.before_create :timestamp_before_create
11
- base.before_update :timestamp_before_update
10
+
11
+ base.class_eval do
12
+ alias_method :create_without_timestamps, :create
13
+ alias_method :create, :create_with_timestamps
14
+
15
+ alias_method :update_without_timestamps, :update
16
+ alias_method :update, :update_with_timestamps
17
+ end
12
18
  end
13
19
 
14
- def timestamp_before_create
20
+ def create_with_timestamps
15
21
  write_attribute("created_at", Time.now) if record_timestamps && respond_to?(:created_at) && created_at.nil?
16
22
  write_attribute("created_on", Time.now) if record_timestamps && respond_to?(:created_on) && created_on.nil?
17
- timestamp_before_update
23
+
24
+ write_attribute("updated_at", Time.now) if record_timestamps && respond_to?(:updated_at)
25
+ write_attribute("updated_on", Time.now) if record_timestamps && respond_to?(:updated_on)
26
+
27
+ create_without_timestamps
18
28
  end
19
29
 
20
- def timestamp_before_update
30
+ def update_with_timestamps
21
31
  write_attribute("updated_at", Time.now) if record_timestamps && respond_to?(:updated_at)
22
32
  write_attribute("updated_on", Time.now) if record_timestamps && respond_to?(:updated_on)
33
+
34
+ update_without_timestamps
23
35
  end
24
36
  end
25
37
 
@@ -77,13 +77,17 @@ module ActiveRecord
77
77
  # Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler.
78
78
  module ClassMethods
79
79
  def transaction(*objects, &block)
80
- TRANSACTION_MUTEX.lock
80
+ TRANSACTION_MUTEX.synchronize do
81
+ Thread.current['open_transactions'] ||= 0
82
+ Thread.current['start_db_transaction'] = (Thread.current['open_transactions'] == 0)
83
+ Thread.current['open_transactions'] += 1
84
+ end
81
85
 
82
86
  begin
83
87
  objects.each { |o| o.extend(Transaction::Simple) }
84
88
  objects.each { |o| o.start_transaction }
85
89
 
86
- result = connection.transaction(&block)
90
+ result = connection.transaction(Thread.current['start_db_transaction'], &block)
87
91
 
88
92
  objects.each { |o| o.commit_transaction }
89
93
  return result
@@ -91,7 +95,9 @@ module ActiveRecord
91
95
  objects.each { |o| o.abort_transaction }
92
96
  raise
93
97
  ensure
94
- TRANSACTION_MUTEX.unlock
98
+ TRANSACTION_MUTEX.synchronize do
99
+ Thread.current['open_transactions'] -= 1
100
+ end
95
101
  end
96
102
  end
97
103
  end
@@ -101,19 +107,11 @@ module ActiveRecord
101
107
  end
102
108
 
103
109
  def destroy_with_transactions #:nodoc:
104
- if TRANSACTION_MUTEX.locked?
105
- destroy_without_transactions
106
- else
107
- transaction { destroy_without_transactions }
108
- end
110
+ transaction { destroy_without_transactions }
109
111
  end
110
112
 
111
113
  def save_with_transactions(perform_validation = true) #:nodoc:
112
- if TRANSACTION_MUTEX.locked?
113
- save_without_transactions(perform_validation)
114
- else
115
- transaction { save_without_transactions(perform_validation) }
116
- end
114
+ transaction { save_without_transactions(perform_validation) }
117
115
  end
118
116
  end
119
117
  end
@@ -74,7 +74,8 @@ module ActiveRecord
74
74
  # <%= password_field "person", "password_confirmation" %>
75
75
  #
76
76
  # The person has to already have a password attribute (a column in the people table), but the password_confirmation is virtual.
77
- # It exists only as an in-memory variable for validating the password. This check is performed both on create and update.
77
+ # It exists only as an in-memory variable for validating the password. This check is performed only if password_confirmation
78
+ # is not nil and by default on save.
78
79
  #
79
80
  # Configuration options:
80
81
  # * <tt>message</tt> - A custom error message (default is: "doesn't match confirmation")
@@ -85,7 +86,7 @@ module ActiveRecord
85
86
 
86
87
  for attr_name in attr_names
87
88
  attr_accessor "#{attr_name}_confirmation"
88
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add('#{attr_name}', "#{configuration[:message]}") unless #{attr_name} == #{attr_name}_confirmation}))
89
+ class_eval(%(#{validation_method(configuration[:on])} %{errors.add('#{attr_name}', "#{configuration[:message]}") unless #{attr_name}_confirmation.nil? or #{attr_name} == #{attr_name}_confirmation}))
89
90
  end
90
91
  end
91
92
 
@@ -96,10 +97,11 @@ module ActiveRecord
96
97
  # validates_acceptance_of :eula, :message => "must be abided"
97
98
  # end
98
99
  #
99
- # The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed both on create and update.
100
+ # The terms_of_service attribute is entirely virtual. No database column is needed. This check is performed only if
101
+ # terms_of_service is not nil and by default on save.
100
102
  #
101
103
  # Configuration options:
102
- # * <tt>message</tt> - A custom error message (default is: "must be accepted")
104
+ # * <tt>message</tt> - A custom error message (default is: "can't be empty")
103
105
  # * <tt>on</tt> - Specifies when this validation is active (default is :save, other options :create, :update)
104
106
  #
105
107
  # NOTE: The agreement is considered valid if it's set to the string "1". This makes it easy to relate it to an HTML checkbox.
@@ -109,11 +111,11 @@ module ActiveRecord
109
111
 
110
112
  for attr_name in attr_names
111
113
  attr_accessor(attr_name)
112
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add('#{attr_name}', '#{configuration[:message]}') unless #{attr_name} == "1"}))
114
+ class_eval(%(#{validation_method(configuration[:on])} %{errors.add('#{attr_name}', '#{configuration[:message]}') unless #{attr_name}.nil? or #{attr_name} == "1"}))
113
115
  end
114
116
  end
115
117
 
116
- # Validates that the specified attributes are neither nil nor empty. Happens by default on both create and update.
118
+ # Validates that the specified attributes are neither nil nor empty. Happens by default on save.
117
119
  #
118
120
  # Configuration options:
119
121
  # * <tt>message</tt> - A custom error message (default is: "has already been taken")
@@ -196,7 +198,7 @@ module ActiveRecord
196
198
  # can be named "davidhh".
197
199
  #
198
200
  # class Person < ActiveRecord::Base
199
- # validates_uniqueness_of :user_name
201
+ # validates_uniqueness_of :user_name, :scope => "account_id"
200
202
  # end
201
203
  #
202
204
  # When the record is created, a check is performed to make sure that no record exist in the database with the given value for the specified
@@ -204,12 +206,17 @@ module ActiveRecord
204
206
  #
205
207
  # Configuration options:
206
208
  # * <tt>message</tt> - Specifies a custom error message (default is: "has already been taken")
209
+ # * <tt>scope</tt> - Ensures that the uniqueness is restricted to a condition of "scope = record.scope"
207
210
  def validates_uniqueness_of(*attr_names)
208
211
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken] }
209
212
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
210
213
 
211
214
  for attr_name in attr_names
212
- class_eval(%(validate %{errors.add("#{attr_name}", "#{configuration[:message]}") if self.class.find_first(new_record? ? ["#{attr_name} = ?", #{attr_name}] : ["#{attr_name} = ? AND id <> ?", #{attr_name}, id])}))
215
+ if scope = configuration[:scope]
216
+ class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ? AND #{scope} = ?', #{attr_name}, #{scope}] : ["#{attr_name} = ? AND id <> ? AND #{scope} = ?", #{attr_name}, id, #{scope}])}))
217
+ else
218
+ class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ?', #{attr_name}] : ["#{attr_name} = ? AND id <> ?", #{attr_name}, id])}))
219
+ end
213
220
  end
214
221
  end
215
222
 
@@ -247,15 +254,21 @@ module ActiveRecord
247
254
  # Configuration options:
248
255
  # * <tt>in</tt> - An enumerable object of available items
249
256
  # * <tt>message</tt> - Specifieds a customer error message (default is: "is not included in the list")
257
+ # * <tt>allows_nil</tt> - If set to true, skips this validation if the attribute is null (default is: false)
250
258
  def validates_inclusion_of(*attr_names)
251
259
  configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save }
252
260
  configuration.update(attr_names.pop) if attr_names.last.is_a?(Hash)
253
261
  enum = configuration[:in] || configuration[:within]
262
+ allow_nil = configuration[:allow_nil]
254
263
 
255
264
  raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?")
256
265
 
257
266
  for attr_name in attr_names
258
- class_eval(%(#{validation_method(configuration[:on])} %{errors.add("#{attr_name}", "#{configuration[:message]}") unless (#{enum.inspect}).include?(#{attr_name}) }))
267
+ if allow_nil
268
+ class_eval(%(#{validation_method(configuration[:on])} %{errors.add("#{attr_name}", "#{configuration[:message]}") unless #{attr_name}.nil? or (#{enum.inspect}).include?(#{attr_name}) }))
269
+ else
270
+ class_eval(%(#{validation_method(configuration[:on])} %{errors.add("#{attr_name}", "#{configuration[:message]}") unless (#{enum.inspect}).include?(#{attr_name}) }))
271
+ end
259
272
  end
260
273
  end
261
274
 
@@ -393,19 +406,24 @@ module ActiveRecord
393
406
 
394
407
  # Will add an error message to each of the attributes in +attributes+ that is empty (defined by <tt>attribute_present?</tt>).
395
408
  def add_on_empty(attributes, msg = @@default_error_messages[:empty])
396
- [attributes].flatten.each { |attr| add(attr, msg) unless @base.attribute_present?(attr.to_s) }
409
+ for attr in [attributes].flatten
410
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
411
+ is_empty = value.respond_to?("empty?") ? value.empty? : false
412
+ add(attr, msg) unless !value.nil? && !is_empty
413
+ end
397
414
  end
398
415
 
399
416
  # Will add an error message to each of the attributes in +attributes+ that has a length outside of the passed boundary +range+.
400
417
  # If the length is above the boundary, the too_long_msg message will be used. If below, the too_short_msg.
401
418
  def add_on_boundary_breaking(attributes, range, too_long_msg = @@default_error_messages[:too_long], too_short_msg = @@default_error_messages[:too_short])
402
419
  for attr in [attributes].flatten
403
- add(attr, too_short_msg % range.begin) if @base[attr.to_s] && @base.send(attr.to_s).length < range.begin
404
- add(attr, too_long_msg % range.end) if @base[attr.to_s] && @base.send(attr.to_s).length > range.end
420
+ value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s]
421
+ add(attr, too_short_msg % range.begin) if value && value.length < range.begin
422
+ add(attr, too_long_msg % range.end) if value && value.length > range.end
405
423
  end
406
424
  end
407
425
 
408
- alias :add_on_boundary_breaking :add_on_boundary_breaking
426
+ alias :add_on_boundry_breaking :add_on_boundary_breaking
409
427
 
410
428
  # Returns true if the specified +attribute+ has errors associated with it.
411
429
  def invalid?(attribute)
data/rakefile CHANGED
@@ -8,7 +8,7 @@ require 'rake/contrib/rubyforgepublisher'
8
8
 
9
9
  PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
10
10
  PKG_NAME = 'activerecord'
11
- PKG_VERSION = '1.2.0' + PKG_BUILD
11
+ PKG_VERSION = '1.3.0' + PKG_BUILD
12
12
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
13
13
 
14
14
  PKG_FILES = FileList[
@@ -4,6 +4,8 @@ $:.unshift(File.dirname(__FILE__) + '/../lib')
4
4
  require 'test/unit'
5
5
  require 'active_record'
6
6
  require 'active_record/fixtures'
7
+ require 'active_record/support/binding_of_caller'
8
+ require 'active_record/support/breakpoint'
7
9
  require 'connection'
8
10
 
9
11
  class Test::Unit::TestCase #:nodoc:
@@ -26,9 +26,7 @@ end
26
26
  class Booleantest < ActiveRecord::Base; end
27
27
 
28
28
  class BasicsTest < Test::Unit::TestCase
29
- def setup
30
- @topic_fixtures, @companies = create_fixtures "topics", "companies"
31
- end
29
+ fixtures :topics, :companies
32
30
 
33
31
  def test_set_attributes
34
32
  topic = Topic.find(1)
@@ -36,7 +34,7 @@ class BasicsTest < Test::Unit::TestCase
36
34
  topic.save
37
35
  assert_equal("Budget", topic.title)
38
36
  assert_equal("Jason", topic.author_name)
39
- assert_equal(@topic_fixtures["first"]["author_email_address"], Topic.find(1).author_email_address)
37
+ assert_equal(@topics["first"]["author_email_address"], Topic.find(1).author_email_address)
40
38
  end
41
39
 
42
40
  def test_integers_as_nil
@@ -187,14 +185,14 @@ class BasicsTest < Test::Unit::TestCase
187
185
  def test_load
188
186
  topics = Topic.find_all nil, "id"
189
187
  assert_equal(2, topics.size)
190
- assert_equal(@topic_fixtures["first"]["title"], topics.first.title)
188
+ assert_equal(@topics["first"]["title"], topics.first.title)
191
189
  end
192
190
 
193
191
  def test_load_with_condition
194
192
  topics = Topic.find_all "author_name = 'Mary'"
195
193
 
196
194
  assert_equal(1, topics.size)
197
- assert_equal(@topic_fixtures["second"]["title"], topics.first.title)
195
+ assert_equal(@topics["second"]["title"], topics.first.title)
198
196
  end
199
197
 
200
198
  def test_table_name_guesses
@@ -262,11 +260,15 @@ class BasicsTest < Test::Unit::TestCase
262
260
  end
263
261
 
264
262
  def test_update_all
265
- Topic.update_all "content = 'bulk updated!'"
263
+ assert_equal 2, Topic.update_all("content = 'bulk updated!'")
266
264
  assert_equal "bulk updated!", Topic.find(1).content
267
265
  assert_equal "bulk updated!", Topic.find(2).content
268
266
  end
269
-
267
+
268
+ def test_delete_all
269
+ assert_equal 2, Topic.delete_all
270
+ end
271
+
270
272
  def test_update_by_condition
271
273
  Topic.update_all "content = 'bulk updated!'", "approved = 1"
272
274
  assert_equal "Have a nice day", Topic.find(1).content
@@ -556,4 +558,22 @@ class BasicsTest < Test::Unit::TestCase
556
558
  topic = Topic.create('content' => content)
557
559
  assert_equal content, Topic.find(topic.id).content
558
560
  end
559
- end
561
+
562
+ def test_class_level_destroy
563
+ should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
564
+ @first.replies << should_be_destroyed_reply
565
+
566
+ Topic.destroy(1)
567
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1) }
568
+ assert_raises(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) }
569
+ end
570
+
571
+ def test_class_level_delete
572
+ should_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
573
+ @first.replies << should_be_destroyed_reply
574
+
575
+ Topic.delete(1)
576
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find(1) }
577
+ assert_nothing_raised { Reply.find(should_be_destroyed_reply.id) }
578
+ end
579
+ end
@@ -36,6 +36,7 @@ CREATE TABLE `topics` (
36
36
  CREATE TABLE `developers` (
37
37
  `id` int(11) NOT NULL auto_increment,
38
38
  `name` varchar(100) default NULL,
39
+ `salary` int(11) default 70000,
39
40
  PRIMARY KEY (`id`)
40
41
  );
41
42
 
@@ -101,10 +102,11 @@ CREATE TABLE `mixins` (
101
102
  `id` int(11) NOT NULL auto_increment,
102
103
  `parent_id` int(11) default NULL,
103
104
  `pos` int(11) default NULL,
104
- `lft` int(11) default NULL,
105
- `rgt` int(11) default NULL,
106
- `root_id` int(11) default NULL,
107
105
  `created_at` datetime default NULL,
108
106
  `updated_at` datetime default NULL,
107
+ `lft` int(11) default NULL,
108
+ `rgt` int(11) default NULL,
109
+ `root_id` int(11) default NULL,
110
+ `type` varchar(40) default NULL,
109
111
  PRIMARY KEY (`id`)
110
112
  );
@@ -29,6 +29,7 @@ CREATE TABLE developers_projects (
29
29
  CREATE TABLE developers (
30
30
  id serial,
31
31
  name character varying(100),
32
+ salary integer DEFAULT 70000,
32
33
  PRIMARY KEY (id)
33
34
  );
34
35
  SELECT setval('developers_id_seq', 100);
@@ -117,6 +118,7 @@ CREATE TABLE colnametests (
117
118
  CREATE TABLE mixins (
118
119
  id serial,
119
120
  parent_id integer,
121
+ type character varying,
120
122
  pos integer,
121
123
  lft integer,
122
124
  rgt integer,
@@ -32,7 +32,8 @@ CREATE TABLE 'topics' (
32
32
 
33
33
  CREATE TABLE 'developers' (
34
34
  'id' INTEGER PRIMARY KEY NOT NULL,
35
- 'name' TEXT DEFAULT NULL
35
+ 'name' TEXT DEFAULT NULL,
36
+ 'salary' INTEGER DEFAULT 70000
36
37
  );
37
38
 
38
39
  CREATE TABLE 'projects' (
@@ -89,6 +90,7 @@ CREATE TABLE 'colnametests' (
89
90
  CREATE TABLE 'mixins' (
90
91
  'id' INTEGER NOT NULL PRIMARY KEY,
91
92
  'parent_id' INTEGER DEFAULT NULL,
93
+ 'type' VARCHAR(40) DEFAULT NULL,
92
94
  'pos' INTEGER DEFAULT NULL,
93
95
  'lft' INTEGER DEFAULT NULL,
94
96
  'rgt' INTEGER DEFAULT NULL,
@@ -41,6 +41,7 @@ CREATE TABLE developers (
41
41
  CREATE TABLE projects (
42
42
  id int NOT NULL IDENTITY(1, 1),
43
43
  name varchar(100) default NULL,
44
+ salary int default 70000,
44
45
  PRIMARY KEY (id)
45
46
  );
46
47
 
@@ -98,6 +99,7 @@ CREATE TABLE colnametests (
98
99
  CREATE TABLE mixins (
99
100
  id int NOT NULL IDENTITY(1, 1),
100
101
  parent_id int default NULL,
102
+ type varchar(40) default NULL,
101
103
  pos int default NULL,
102
104
  lft int default NULL,
103
105
  rgt int default NULL,
@@ -1,8 +1,6 @@
1
1
  class Developer < ActiveRecord::Base
2
2
  has_and_belongs_to_many :projects
3
3
 
4
- protected
5
- def validate
6
- errors.add_on_boundary_breaking("name", 3..20)
7
- end
4
+ validates_inclusion_of :salary, :in => 50000..200000
5
+ validates_length_of :name, :within => 3..20
8
6
  end
@@ -1,13 +1,16 @@
1
1
  david:
2
2
  id: 1
3
3
  name: David
4
+ salary: 80000
4
5
 
5
6
  jamis:
6
7
  id: 2
7
8
  name: Jamis
9
+ salary: 150000
8
10
 
9
11
  <% for digit in 3..10 %>
10
12
  dev_<%= digit %>:
11
13
  id: <%= digit %>
12
14
  name: fixture_<%= digit %>
15
+ salary: 100000
13
16
  <% end %>
@@ -1,9 +1,12 @@
1
1
  class Mixin < ActiveRecord::Base
2
- acts_as_tree :foreign_key => "parent_id", :order => "id"
3
2
 
4
3
  end
5
4
 
6
- class ListMixin < ActiveRecord::Base
5
+ class TreeMixin < Mixin
6
+ acts_as_tree :foreign_key => "parent_id", :order => "id"
7
+ end
8
+
9
+ class ListMixin < Mixin
7
10
  acts_as_list :column => "pos", :scope => :parent
8
11
 
9
12
  def self.table_name() "mixins" end
@@ -1,14 +1,30 @@
1
- first:
2
- id: 1
3
- pos: 1
1
+ # tree mixins
2
+ tree_1:
3
+ id: 1001
4
+ type: TreeMixin
4
5
  parent_id: 0
5
6
 
6
- second:
7
- id: 2
8
- pos: 1
9
- parent_id: 1
7
+ tree_2:
8
+ id: 1002
9
+ type: TreeMixin
10
+ parent_id: 1001
10
11
 
11
- third:
12
- id: 3
13
- pos: 2
14
- parent_id: 1
12
+ tree_3:
13
+ id: 1003
14
+ type: TreeMixin
15
+ parent_id: 1002
16
+
17
+ tree_4:
18
+ id: 1004
19
+ type: TreeMixin
20
+ parent_id: 1001
21
+
22
+ # List mixins
23
+
24
+ <% (1..4).each do |counter| %>
25
+ list_<%= counter %>:
26
+ id: <%= counter+1006 %>
27
+ pos: <%= counter %>
28
+ type: ListMixin
29
+ parent_id: 5
30
+ <% end %>