custom fields proof of concept in progress

This commit is contained in:
dinedine 2010-05-19 18:17:45 +02:00
parent 277b531449
commit 05a93cb5c1
14 changed files with 339 additions and 91 deletions

View File

@ -5,9 +5,9 @@ source "http://gems.github.com"
gem "rails", "3.0.0.beta3" gem "rails", "3.0.0.beta3"
gem "liquid" gem "liquid"
gem "bson_ext", '0.20.1' gem "bson_ext", ">= 1.0.1"
gem "mongo_ext" 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 "mongoid_acts_as_tree", :git => 'git://github.com/evansagge/mongoid_acts_as_tree.git'
gem "warden" gem "warden"
gem "devise", ">= 1.1.rc0" gem "devise", ">= 1.1.rc0"

View 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

View File

@ -15,7 +15,7 @@ class AssetCollection
embeds_many :asset_fields # FIXME (custom fields) embeds_many :asset_fields # FIXME (custom fields)
## behaviours ## ## behaviours ##
accepts_nested_attributes_for :asset_fields # FIXME (custom fields) accepts_nested_attributes_for :asset_fields, :allow_destroy => true # FIXME (custom fields)
## callbacks ## ## callbacks ##
before_validate :normalize_slug before_validate :normalize_slug
@ -39,6 +39,10 @@ class AssetCollection
@assets_order = order @assets_order = order
end end
def ordered_asset_fields # FIXME (custom fields)
self.asset_fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
end
protected protected
def normalize_slug def normalize_slug

View File

@ -12,6 +12,8 @@ class AssetField
## validations ## ## validations ##
validates_presence_of :label, :kind validates_presence_of :label, :kind
embedded_in :asset_collection, :inverse_of => :asset_fields
## methods ## ## methods ##
def field_type def field_type
@ -49,7 +51,7 @@ class AssetField
end end
def increment_counter! 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) self._parent.send(:"#{self.association_name}_counter=", next_value)
next_value next_value
end end

View 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
&mdash;
%em= t("admin.custom_fields.kind.#{field.kind.downcase}")
= g.select :kind, options_for_field_kind
&nbsp;
%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'
&mdash;
%em
= g.select :kind, options_for_field_kind
&nbsp;
%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')

View File

@ -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') - 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 - 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 - content_for :submenu do
= render 'admin/shared/menu/assets' = render 'admin/shared/menu/assets'
@ -25,4 +25,6 @@
= f.input :name = f.input :name
= f.input :slug, :required => false = 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 = 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

View File

@ -24,7 +24,7 @@
%span.actions %span.actions
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm') = link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove first', :confirm => t('admin.messages.confirm')
%li.item.new %li.item.template
%em %em
http:// http://
= text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void' = text_field_tag 'label', t('formtastic.hints.site.domain_name'), :class => 'string label void'

View File

@ -50,7 +50,10 @@ module Mongoid #:nodoc:
if self.custom_fields?(object, association_name) if self.custom_fields?(object, association_name)
# puts "custom fields = #{object.asset_fields.inspect}" # puts "custom fields = #{object.asset_fields.inspect}"
# puts "((((((((" # 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}" # puts "field = #{field.inspect}"
# self.class.send(:set_field, field.name, { :type => field.field_type }) # self.class.send(:set_field, field.name, { :type => field.field_type })
field.apply(self, association_name) field.apply(self, association_name)

View File

@ -32,6 +32,11 @@ en:
create: Create create: Create
update: Update update: Update
custom_fields:
kind:
string: Simple Input
text: Text
sessions: sessions:
new: new:
title: Login title: Login
@ -149,6 +154,7 @@ en:
file: File file: File
preview: Preview preview: Preview
options: Advanced options options: Advanced options
custom_fields: Custom fields
labels: labels:
theme_asset: theme_asset:
new: new:

View File

@ -57,6 +57,9 @@ x domain scoping when authenticating
- extract a plugin from custom fields - extract a plugin from custom fields
- field position - field position
- nested attributes - nested attributes
- keep tracks of all custom fields (adding / editing assets)
- custom fields -> metadata keys
- duplicate fields
BACKLOG: BACKLOG:
- liquid rendering engine - liquid rendering engine

View 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
});
});

View File

@ -1,20 +1,20 @@
$(document).ready(function() { $(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 ___ */ /* __ fields ___ */
$('fieldset.editable-list li.new input[type=text]').focus(function() { $('fieldset.editable-list li.template input[type=text]').focus(function() {
if ($(this).hasClass('void') && $(this).parents('li').hasClass('new')) if ($(this).hasClass('void') && $(this).parents('li').hasClass('template'))
$(this).val('').removeClass('void'); $(this).val('').removeClass('void');
}); });
$('fieldset.editable-list li.new button').click(function() { $('fieldset.editable-list li.template button').click(function() {
var lastRow = $(this).parents('li.new'); var lastRow = $(this).parents('li.template');
var currentValue = lastRow.find('input.label').val(); var currentValue = lastRow.find('input.label').val();
if (currentValue == defaultValue || currentValue == '') return; 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 // should copy the value of the select box
var input = newRow.find('input.label') var input = newRow.find('input.label')

View File

@ -302,13 +302,13 @@ form.formtastic fieldset.editable-list ol li.added .inline-errors {
font-size: 0.8em; font-size: 0.8em;
} }
form.formtastic fieldset.editable-list ol li.new { form.formtastic fieldset.editable-list ol li.template {
height: 42px; height: 42px;
background-image: url(/images/admin/form/big_item.png); background-image: url(/images/admin/form/big_item.png);
padding-top: 10px; padding-top: 10px;
} }
form.formtastic fieldset.editable-list ol li.new input { form.formtastic fieldset.editable-list ol li.template input {
display: inline; display: inline;
margin-left: 10px; margin-left: 10px;
padding: 4px; padding: 4px;
@ -321,28 +321,28 @@ form.formtastic fieldset.editable-list ol li.new input {
top: 1px; top: 1px;
} }
form.formtastic fieldset.editable-list ol li.new select { form.formtastic fieldset.editable-list ol li.template select {
display: inline; display: inline;
} }
form.formtastic fieldset.editable-list ol li.new span.handle { form.formtastic fieldset.editable-list ol li.template span.handle {
display: none; display: none;
} }
form.formtastic fieldset.editable-list ol li.new span.actions { form.formtastic fieldset.editable-list ol li.template span.actions {
width: auto; width: auto;
top: 10px; 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; 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; 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; font-size: 0.8em;
} }

View File

@ -12,84 +12,138 @@ describe AssetCollection do
@collection = Factory.build(:asset_collection, :site => nil) @collection = Factory.build(:asset_collection, :site => nil)
@collection.asset_fields.build :label => 'My Description', :_alias => 'description', :kind => 'Text' @collection.asset_fields.build :label => 'My Description', :_alias => 'description', :kind => 'Text'
@collection.asset_fields.build :label => 'Active', :kind => 'Boolean' @collection.asset_fields.build :label => 'Active', :kind => 'Boolean'
puts "first field index = #{@collection.asset_fields.first._index}"
end 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 context 'managing from hash' 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 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 end
it 'should add new field' do # it 'should add new field' do
@collection.asset_fields.build :label => 'Active at', :name => 'active_at', :kind => 'Date' # @collection.asset_fields.clear
@collection.upsert(false) # @collection.asset_fields_attributes = { 'NEW_RECORD' => { 'label' => 'Tagline', 'kind' => 'String' } }
@collection.reload # @collection.asset_fields.first.label.should == 'Tagline'
asset = @collection.assets.first # end
lambda { asset.active_at }.should_not raise_error #
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 it 'should remove field' do
@collection.asset_fields.clear @collection.asset_fields.build :label => 'Title', :kind => 'String'
@collection.upsert(false) # puts @collection.asset_fields.collect { |d| d._index }.inspect
@collection.reload @collection.save
asset = @collection.assets.first @collection = AssetCollection.first
lambda { asset.active }.should raise_error foo = @collection.asset_fields
end @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 puts @collection.raw_attributes.inspect
@collection.asset_fields.first.label = 'Simple description'
@collection.asset_fields.first._alias = nil # @collection.save
@collection.upsert(false) @collection = AssetCollection.first
@collection.reload # @collection.reload
asset = @collection.assets.first puts "________________ #{@collection.asset_fields.class.inspect}"
asset.simple_description.should == 'Lorem ipsum' puts @collection.asset_fields.inspect
@collection.asset_fields.size.should == 1
@collection.asset_fields.first.label.should == 'My Title !'
end end
end end