custom fields proof of concept in progress
This commit is contained in:
parent
277b531449
commit
05a93cb5c1
4
Gemfile
4
Gemfile
@ -5,9 +5,9 @@ source "http://gems.github.com"
|
||||
gem "rails", "3.0.0.beta3"
|
||||
|
||||
gem "liquid"
|
||||
gem "bson_ext", '0.20.1'
|
||||
gem "bson_ext", ">= 1.0.1"
|
||||
gem "mongo_ext"
|
||||
gem "mongoid", ">= 2.0.0.beta4"
|
||||
gem "mongoid", ">= 2.0.0.beta6"
|
||||
gem "mongoid_acts_as_tree", :git => 'git://github.com/evansagge/mongoid_acts_as_tree.git'
|
||||
gem "warden"
|
||||
gem "devise", ">= 1.1.rc0"
|
||||
|
18
app/helpers/admin/custom_fields_helper.rb
Normal file
18
app/helpers/admin/custom_fields_helper.rb
Normal file
@ -0,0 +1,18 @@
|
||||
module Admin::CustomFieldsHelper
|
||||
|
||||
# def options_for_field_kind(selected = nil)
|
||||
# # %w{String Text Boolean Email File Date}
|
||||
# options = %w{String Text}.map do |kind|
|
||||
# [t("admin.custom_fields.kind.#{kind.downcase}"), kind]
|
||||
# end
|
||||
# options_for_select(options, selected)
|
||||
# end
|
||||
|
||||
def options_for_field_kind(selected = nil)
|
||||
# %w{String Text Boolean Email File Date}
|
||||
options = %w{String Text}.map do |kind|
|
||||
[t("admin.custom_fields.kind.#{kind.downcase}"), kind]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
@ -15,7 +15,7 @@ class AssetCollection
|
||||
embeds_many :asset_fields # FIXME (custom fields)
|
||||
|
||||
## behaviours ##
|
||||
accepts_nested_attributes_for :asset_fields # FIXME (custom fields)
|
||||
accepts_nested_attributes_for :asset_fields, :allow_destroy => true # FIXME (custom fields)
|
||||
|
||||
## callbacks ##
|
||||
before_validate :normalize_slug
|
||||
@ -39,6 +39,10 @@ class AssetCollection
|
||||
@assets_order = order
|
||||
end
|
||||
|
||||
def ordered_asset_fields # FIXME (custom fields)
|
||||
self.asset_fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def normalize_slug
|
||||
|
@ -12,6 +12,8 @@ class AssetField
|
||||
## validations ##
|
||||
validates_presence_of :label, :kind
|
||||
|
||||
embedded_in :asset_collection, :inverse_of => :asset_fields
|
||||
|
||||
## methods ##
|
||||
|
||||
def field_type
|
||||
@ -49,7 +51,7 @@ class AssetField
|
||||
end
|
||||
|
||||
def increment_counter!
|
||||
next_value = self._parent.send(:"#{self.association_name}_counter") + 1
|
||||
next_value = (self._parent.send(:"#{self.association_name}_counter") || 0) + 1
|
||||
self._parent.send(:"#{self.association_name}_counter=", next_value)
|
||||
next_value
|
||||
end
|
||||
|
43
app/views/admin/asset_collections/_custom_fields.html.haml
Normal file
43
app/views/admin/asset_collections/_custom_fields.html.haml
Normal file
@ -0,0 +1,43 @@
|
||||
= f.foldable_inputs :name => :custom_fields, :class => 'editable-list fields off' do
|
||||
- f.object.ordered_asset_fields.each do |field|
|
||||
= f.fields_for :asset_fields, field, :child_index => field._index do |g|
|
||||
%li{ :class => "item added #{'error' unless field.errors.empty?}"}
|
||||
%span.handle
|
||||
= image_tag 'admin/form/icons/drag.png'
|
||||
|
||||
= g.hidden_field :position, :class => 'position'
|
||||
|
||||
= g.text_field :label
|
||||
|
||||
—
|
||||
|
||||
%em= t("admin.custom_fields.kind.#{field.kind.downcase}")
|
||||
|
||||
= g.select :kind, options_for_field_kind
|
||||
|
||||
|
||||
|
||||
%span.actions
|
||||
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
|
||||
|
||||
= f.fields_for :asset_fields, @collection.asset_fields.build(:label => 'field name'), :child_index => '-1' do |g|
|
||||
%li{ :class => 'item template' }
|
||||
%span.handle
|
||||
= image_tag 'admin/form/icons/drag.png'
|
||||
|
||||
= g.hidden_field :position, :class => 'position'
|
||||
|
||||
= g.text_field :label, :class => 'string label void'
|
||||
|
||||
—
|
||||
|
||||
%em
|
||||
|
||||
= g.select :kind, options_for_field_kind
|
||||
|
||||
|
||||
|
||||
%span.actions
|
||||
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
|
||||
%button{ :class => 'button light add', :type => 'button' }
|
||||
%span= t('admin.buttons.new_item')
|
@ -1,7 +1,7 @@
|
||||
- title link_to(@collection.name.blank? ? @collection.name_was : @collection.name, '#', :rel => 'asset_collection_name', :title => t('.ask_for_name'), :class => 'editable')
|
||||
|
||||
- content_for :head do
|
||||
= javascript_include_tag 'admin/asset_collections.js'
|
||||
= javascript_include_tag 'admin/asset_collections.js', 'admin/custom_fields'
|
||||
|
||||
- content_for :submenu do
|
||||
= render 'admin/shared/menu/assets'
|
||||
@ -25,4 +25,6 @@
|
||||
= f.input :name
|
||||
= f.input :slug, :required => false
|
||||
|
||||
= render 'custom_fields', :f => f
|
||||
|
||||
= render 'admin/shared/form_actions', :delete_button => link_to(content_tag(:span, t('.destroy')), admin_asset_collection_url(@collection), :confirm => t('admin.messages.confirm'), :method => :delete, :class => 'button small remove'), :button_label => :update
|
@ -24,7 +24,7 @@
|
||||
%span.actions
|
||||
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
|
||||
|
||||
%li.item.new
|
||||
%li.item.template
|
||||
%em
|
||||
http://
|
||||
= text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void'
|
||||
|
@ -50,7 +50,10 @@ module Mongoid #:nodoc:
|
||||
if self.custom_fields?(object, association_name)
|
||||
# puts "custom fields = #{object.asset_fields.inspect}"
|
||||
# puts "(((((((("
|
||||
object.send(self.custom_fields_association_name(association_name)).each do |field|
|
||||
|
||||
# puts " custom fields = #{self.custom_fields_association_name(association_name).inspect} / #{object.send(self.custom_fields_association_name(association_name)).inspect}"
|
||||
|
||||
[*object.send(self.custom_fields_association_name(association_name))].each do |field|
|
||||
# puts "field = #{field.inspect}"
|
||||
# self.class.send(:set_field, field.name, { :type => field.field_type })
|
||||
field.apply(self, association_name)
|
||||
|
@ -32,6 +32,11 @@ en:
|
||||
create: Create
|
||||
update: Update
|
||||
|
||||
custom_fields:
|
||||
kind:
|
||||
string: Simple Input
|
||||
text: Text
|
||||
|
||||
sessions:
|
||||
new:
|
||||
title: Login
|
||||
@ -149,6 +154,7 @@ en:
|
||||
file: File
|
||||
preview: Preview
|
||||
options: Advanced options
|
||||
custom_fields: Custom fields
|
||||
labels:
|
||||
theme_asset:
|
||||
new:
|
||||
|
3
doc/TODO
3
doc/TODO
@ -57,6 +57,9 @@ x domain scoping when authenticating
|
||||
- extract a plugin from custom fields
|
||||
- field position
|
||||
- nested attributes
|
||||
- keep tracks of all custom fields (adding / editing assets)
|
||||
- custom fields -> metadata keys
|
||||
- duplicate fields
|
||||
|
||||
BACKLOG:
|
||||
- liquid rendering engine
|
||||
|
113
public/javascripts/admin/custom_fields.js
Normal file
113
public/javascripts/admin/custom_fields.js
Normal file
@ -0,0 +1,113 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
$('fieldset.fields').parents('form').submit(function() {
|
||||
$('fieldset.fields li.template input, fieldset.fields li.template select').attr('disabled', 'disabled');
|
||||
});
|
||||
|
||||
var defaultValue = $('fieldset.fields li.template input[type=text]').val();
|
||||
var selectOnChange = function(select) {
|
||||
select.hide();
|
||||
select.prev()
|
||||
.show()
|
||||
.html(select[0].options[select[0].options.selectedIndex].text);
|
||||
}
|
||||
|
||||
var refreshPosition = function() {
|
||||
jQuery.each($('fieldset.fields li.added input.position'), function(index) {
|
||||
$(this).val(index);
|
||||
});
|
||||
}
|
||||
|
||||
/* __ fields ___ */
|
||||
$('fieldset.fields li.added select').each(function() {
|
||||
var select = $(this)
|
||||
.hover(function() {
|
||||
clearTimeout(select.attr('timer'));
|
||||
}, function() {
|
||||
select.attr('timer', setTimeout(function() {
|
||||
select.hide();
|
||||
select.prev().show();
|
||||
}, 1000));
|
||||
})
|
||||
.change(function() { selectOnChange(select); });
|
||||
|
||||
select.prev().click(function() {
|
||||
$(this).hide();
|
||||
select.show();
|
||||
});
|
||||
});
|
||||
|
||||
$('fieldset.fields li.template input[type=text]').focus(function() {
|
||||
if ($(this).hasClass('void') && $(this).parents('li').hasClass('template'))
|
||||
$(this).val('').removeClass('void');
|
||||
});
|
||||
|
||||
$('fieldset.fields li.template button').click(function() {
|
||||
var lastRow = $(this).parents('li.template');
|
||||
var newRow = lastRow.clone(true).removeClass('template').addClass('added new').insertBefore(lastRow);
|
||||
|
||||
var dateFragment = '[' + new Date().getTime() + ']';
|
||||
newRow.find('input, select').each(function(index) {
|
||||
$(this).attr('name', $(this).attr('name').replace('[-1]', dateFragment));
|
||||
});
|
||||
|
||||
// should copy the value of the select box
|
||||
var input = newRow.find('input.label');
|
||||
if (lastRow.find('input.label').val() == '') input.val("undefined");
|
||||
|
||||
var select = newRow.find('select')
|
||||
.val(lastRow.find('select').val())
|
||||
.change(function() { selectOnChange(select); })
|
||||
.hover(function() {
|
||||
clearTimeout(select.attr('timer'));
|
||||
}, function() {
|
||||
select.attr('timer', setTimeout(function() {
|
||||
select.hide();
|
||||
select.prev().show();
|
||||
}, 1000));
|
||||
});
|
||||
select.prev()
|
||||
.html(select[0].options[select[0].options.selectedIndex].text)
|
||||
.click(function() {
|
||||
$(this).hide();
|
||||
select.show();
|
||||
});
|
||||
|
||||
// then reset the form
|
||||
lastRow.find('input').val(defaultValue).addClass('void');
|
||||
lastRow.find('select').val('input');
|
||||
|
||||
// warn the sortable widget about the new row
|
||||
$("fieldset.fields ol").sortable('refresh');
|
||||
|
||||
refreshPosition();
|
||||
});
|
||||
|
||||
$('fieldset.fields li a.remove').click(function(e) {
|
||||
if (confirm($(this).attr('data-confirm'))) {
|
||||
var parent = $(this).parents('li');
|
||||
|
||||
if (parent.hasClass('new'))
|
||||
parent.remove();
|
||||
else {
|
||||
var field = parent.find('input.position')
|
||||
field.attr('name', field.attr('name').replace('[position]', '[_destroy]'));
|
||||
|
||||
parent.hide().removeClass('added')
|
||||
}
|
||||
|
||||
refreshPosition();
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
// sortable list
|
||||
$("fieldset.fields ol").sortable({
|
||||
handle: 'span.handle',
|
||||
items: 'li:not(.template)',
|
||||
axis: 'y',
|
||||
update: refreshPosition
|
||||
});
|
||||
});
|
@ -1,20 +1,20 @@
|
||||
$(document).ready(function() {
|
||||
|
||||
var defaultValue = $('fieldset.editable-list li.new input[type=text]').val();
|
||||
var defaultValue = $('fieldset.editable-list li.template input[type=text]').val();
|
||||
|
||||
/* __ fields ___ */
|
||||
$('fieldset.editable-list li.new input[type=text]').focus(function() {
|
||||
if ($(this).hasClass('void') && $(this).parents('li').hasClass('new'))
|
||||
$('fieldset.editable-list li.template input[type=text]').focus(function() {
|
||||
if ($(this).hasClass('void') && $(this).parents('li').hasClass('template'))
|
||||
$(this).val('').removeClass('void');
|
||||
});
|
||||
|
||||
$('fieldset.editable-list li.new button').click(function() {
|
||||
var lastRow = $(this).parents('li.new');
|
||||
$('fieldset.editable-list li.template button').click(function() {
|
||||
var lastRow = $(this).parents('li.template');
|
||||
|
||||
var currentValue = lastRow.find('input.label').val();
|
||||
if (currentValue == defaultValue || currentValue == '') return;
|
||||
|
||||
var newRow = lastRow.clone(true).removeClass('new').addClass('added').insertBefore(lastRow);
|
||||
var newRow = lastRow.clone(true).removeClass('template').addClass('added').insertBefore(lastRow);
|
||||
|
||||
// should copy the value of the select box
|
||||
var input = newRow.find('input.label')
|
||||
|
@ -302,13 +302,13 @@ form.formtastic fieldset.editable-list ol li.added .inline-errors {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new {
|
||||
form.formtastic fieldset.editable-list ol li.template {
|
||||
height: 42px;
|
||||
background-image: url(/images/admin/form/big_item.png);
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new input {
|
||||
form.formtastic fieldset.editable-list ol li.template input {
|
||||
display: inline;
|
||||
margin-left: 10px;
|
||||
padding: 4px;
|
||||
@ -321,28 +321,28 @@ form.formtastic fieldset.editable-list ol li.new input {
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new select {
|
||||
form.formtastic fieldset.editable-list ol li.template select {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new span.handle {
|
||||
form.formtastic fieldset.editable-list ol li.template span.handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new span.actions {
|
||||
form.formtastic fieldset.editable-list ol li.template span.actions {
|
||||
width: auto;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new span.actions a.remove {
|
||||
form.formtastic fieldset.editable-list ol li.template span.actions a.remove {
|
||||
display: none;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new span.actions button {
|
||||
form.formtastic fieldset.editable-list ol li.template span.actions button {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
form.formtastic fieldset.editable-list ol li.new span.actions button span {
|
||||
form.formtastic fieldset.editable-list ol li.template span.actions button span {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
|
@ -12,84 +12,138 @@ describe AssetCollection do
|
||||
@collection = Factory.build(:asset_collection, :site => nil)
|
||||
@collection.asset_fields.build :label => 'My Description', :_alias => 'description', :kind => 'Text'
|
||||
@collection.asset_fields.build :label => 'Active', :kind => 'Boolean'
|
||||
puts "first field index = #{@collection.asset_fields.first._index}"
|
||||
end
|
||||
|
||||
context 'define core attributes' do
|
||||
# context 'define core attributes' do
|
||||
#
|
||||
# it 'should have an unique name' do
|
||||
# @collection.asset_fields.first._name.should == "custom_field_1"
|
||||
# @collection.asset_fields.last._name.should == "custom_field_2"
|
||||
# end
|
||||
#
|
||||
# it 'should have an unique alias' do
|
||||
# @collection.asset_fields.first._alias.should == "description"
|
||||
# @collection.asset_fields.last._alias.should == "active"
|
||||
# end
|
||||
#
|
||||
# end
|
||||
#
|
||||
# context 'build and save' do
|
||||
#
|
||||
# it 'should build asset' do
|
||||
# asset = @collection.assets.build
|
||||
# lambda {
|
||||
# asset.description
|
||||
# asset.active
|
||||
# }.should_not raise_error
|
||||
# end
|
||||
#
|
||||
# it 'should assign values to newly built asset' do
|
||||
# asset = build_asset(@collection)
|
||||
# asset.description.should == 'Lorem ipsum'
|
||||
# asset.active.should == true
|
||||
# end
|
||||
#
|
||||
# it 'should save asset' do
|
||||
# asset = build_asset(@collection)
|
||||
# asset.save and @collection.reload
|
||||
# asset = @collection.assets.first
|
||||
# asset.description.should == 'Lorem ipsum'
|
||||
# asset.active.should == true
|
||||
# end
|
||||
#
|
||||
# it 'should not modify assets from another collection' do
|
||||
# asset = build_asset(@collection)
|
||||
# asset.save and @collection.reload
|
||||
# new_collection = AssetCollection.new
|
||||
# lambda { new_collection.assets.build.description }.should raise_error
|
||||
# end
|
||||
#
|
||||
# end
|
||||
#
|
||||
# context 'modifying fields' do
|
||||
#
|
||||
# before(:each) do
|
||||
# @asset = build_asset(@collection).save
|
||||
# end
|
||||
#
|
||||
# it 'should add new field' do
|
||||
# @collection.asset_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date'
|
||||
# @collection.upsert(false)
|
||||
# @collection.reload
|
||||
# asset = @collection.assets.first
|
||||
# lambda { asset.active_at }.should_not raise_error
|
||||
# end
|
||||
#
|
||||
# it 'should remove field' do
|
||||
# @collection.asset_fields.clear
|
||||
# @collection.upsert(false)
|
||||
# @collection.reload
|
||||
# asset = @collection.assets.first
|
||||
# lambda { asset.active }.should raise_error
|
||||
# end
|
||||
#
|
||||
# it 'should rename field label' do
|
||||
# @collection.asset_fields.first.label = 'Simple description'
|
||||
# @collection.asset_fields.first._alias = nil
|
||||
# @collection.upsert(false)
|
||||
# @collection.reload
|
||||
# asset = @collection.assets.first
|
||||
# asset.simple_description.should == 'Lorem ipsum'
|
||||
# end
|
||||
#
|
||||
# end
|
||||
|
||||
it 'should have an unique name' do
|
||||
@collection.asset_fields.first._name.should == "custom_field_1"
|
||||
@collection.asset_fields.last._name.should == "custom_field_2"
|
||||
end
|
||||
|
||||
it 'should have an unique alias' do
|
||||
@collection.asset_fields.first._alias.should == "description"
|
||||
@collection.asset_fields.last._alias.should == "active"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'build and save' do
|
||||
|
||||
it 'should build asset' do
|
||||
asset = @collection.assets.build
|
||||
lambda {
|
||||
asset.description
|
||||
asset.active
|
||||
}.should_not raise_error
|
||||
end
|
||||
|
||||
it 'should assign values to newly built asset' do
|
||||
asset = build_asset(@collection)
|
||||
asset.description.should == 'Lorem ipsum'
|
||||
asset.active.should == true
|
||||
end
|
||||
|
||||
it 'should save asset' do
|
||||
asset = build_asset(@collection)
|
||||
asset.save and @collection.reload
|
||||
asset = @collection.assets.first
|
||||
asset.description.should == 'Lorem ipsum'
|
||||
asset.active.should == true
|
||||
end
|
||||
|
||||
it 'should not modify assets from another collection' do
|
||||
asset = build_asset(@collection)
|
||||
asset.save and @collection.reload
|
||||
new_collection = AssetCollection.new
|
||||
lambda { new_collection.assets.build.description }.should raise_error
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context 'modifying fields' do
|
||||
context 'managing from hash' do
|
||||
|
||||
before(:each) do
|
||||
@asset = build_asset(@collection).save
|
||||
# @collection.asset_fields.clear
|
||||
# @collection.stubs(:validate).returns(true)
|
||||
# @collection.stubs(:valid?).returns(true)
|
||||
@collection.site = Factory(:site)
|
||||
end
|
||||
|
||||
it 'should add new field' do
|
||||
@collection.asset_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date'
|
||||
@collection.upsert(false)
|
||||
@collection.reload
|
||||
asset = @collection.assets.first
|
||||
lambda { asset.active_at }.should_not raise_error
|
||||
end
|
||||
# it 'should add new field' do
|
||||
# @collection.asset_fields.clear
|
||||
# @collection.asset_fields_attributes = { 'NEW_RECORD' => { 'label' => 'Tagline', 'kind' => 'String' } }
|
||||
# @collection.asset_fields.first.label.should == 'Tagline'
|
||||
# end
|
||||
#
|
||||
# it 'should add new field' do
|
||||
# @collection.asset_fields.build :label => 'Title'
|
||||
# @collection.asset_fields_attributes = { '0' => { 'label' => 'A title', 'kind' => 'String' }, '-1' => { 'label' => 'Tagline', 'kind' => 'String' } }
|
||||
# @collection.asset_fields.size.should == 2
|
||||
# @collection.asset_fields.first.label.should == 'A title'
|
||||
# @collection.asset_fields.last.label.should == 'Tagline'
|
||||
# end
|
||||
#
|
||||
# it 'should rename field'
|
||||
|
||||
it 'should remove field' do
|
||||
@collection.asset_fields.clear
|
||||
@collection.upsert(false)
|
||||
@collection.reload
|
||||
asset = @collection.assets.first
|
||||
lambda { asset.active }.should raise_error
|
||||
end
|
||||
@collection.asset_fields.build :label => 'Title', :kind => 'String'
|
||||
# puts @collection.asset_fields.collect { |d| d._index }.inspect
|
||||
@collection.save
|
||||
@collection = AssetCollection.first
|
||||
foo = @collection.asset_fields
|
||||
@collection.asset_fields.size.should == 3
|
||||
@collection.update_attributes(:asset_fields_attributes => {
|
||||
# @collection.asset_fields_attributes = {
|
||||
'0' => { 'label' => 'My Description', 'kind' => 'Text', '_destroy' => "1", "id" => foo[0].id },
|
||||
'1' => { 'label' => 'Active', 'kind' => 'Boolean', '_destroy' => "0", "id" => foo[1].id },
|
||||
'2' => { 'label' => 'My Title !', 'kind' => 'String', "id" => foo[2].id }
|
||||
# }
|
||||
})
|
||||
|
||||
it 'should rename field label' do
|
||||
@collection.asset_fields.first.label = 'Simple description'
|
||||
@collection.asset_fields.first._alias = nil
|
||||
@collection.upsert(false)
|
||||
@collection.reload
|
||||
asset = @collection.assets.first
|
||||
asset.simple_description.should == 'Lorem ipsum'
|
||||
puts @collection.raw_attributes.inspect
|
||||
|
||||
# @collection.save
|
||||
@collection = AssetCollection.first
|
||||
# @collection.reload
|
||||
puts "________________ #{@collection.asset_fields.class.inspect}"
|
||||
puts @collection.asset_fields.inspect
|
||||
@collection.asset_fields.size.should == 1
|
||||
@collection.asset_fields.first.label.should == 'My Title !'
|
||||
end
|
||||
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user