rename CustomFields::CustomField into CustomFields::Field + create a dynamic class when we have an object with custom fields (optimization) + create a new type of custom field: categories + fix minor bugs + add ui to rename the alias of a custom field

This commit is contained in:
dinedine 2010-06-08 02:45:49 +02:00
parent 3e7f18e8e3
commit fc690d8a0b
58 changed files with 792 additions and 156 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ pkg
*.gemspec
rails_3_gems
doc/performance.txt
doc/production.sh

10
Gemfile
View File

@ -11,20 +11,14 @@ gem "mongoid", ">= 2.0.0.beta6"
gem "mongoid_acts_as_tree", ">= 0.1.2"
gem "warden"
gem "devise", ">= 1.1.rc0"
gem "haml", '>= 3.0.1'
# gem "rmagick"
gem "haml", ">= 3.0.1"
gem "rmagick", "2.12.2"
gem "aws"
gem "jeweler"
gem "mimetype-fu", :require => "mimetype_fu"
gem "formtastic-rails3", :require => "formtastic"
gem "carrierwave-rails3", :require => "carrierwave"
# gem 'formtastic-rails3', :require => 'formtastic', :path => 'rails_3_gems/formtastic'
# gem "formtastic", :git => 'git://github.com/justinfrench/formtastic.git', :branch => 'rails3'
# gem "carrierwave", :git => "http://github.com/jnicklas/carrierwave.git"
# gem "carrierwave", :path => 'rails_3_gems/carrierwave'
# Development environment
group :development do
# Using mongrel instead of webrick (default server)

View File

@ -1 +1 @@
0.0.1.3
0.0.1.4

View File

@ -45,7 +45,7 @@ module Admin
flash[:error] = e.to_s
end
redirect_to admin_content_types_url
redirect_to admin_pages_url
end
end

View File

@ -2,7 +2,7 @@ module Admin::CustomFieldsHelper
def options_for_field_kind(selected = nil)
# %w{String Text Boolean Email File Date}
options = %w{String Text}.map do |kind|
options = %w{String Text Select}.map do |kind|
[t("admin.custom_fields.kind.#{kind.downcase}"), kind]
end
end

View File

@ -1,10 +1,11 @@
class Asset
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Timestamps
## Extensions ##
include Models::Extensions::Asset::Vignette
include CustomFields::ProxyClassEnabler
## fields ##
field :name, :type => String

View File

@ -53,6 +53,8 @@ class ThemeAsset
end
def performing_plain_text?
return true if !self.new_record? && !self.image? && self.errors.empty?
!(self.performing_plain_text.blank? || self.performing_plain_text == 'false' || self.performing_plain_text == false)
end

View File

@ -35,8 +35,8 @@ class AssetUploader < CarrierWave::Uploader::Base
self.class.content_types.each_pair do |type, rules|
rules.each do |rule|
case rule
when String then value = type if file.content_type == rule
when Regexp then value = type if (file.content_type =~ rule) == 0
when String then value = type if content_type == rule
when Regexp then value = type if (content_type =~ rule) == 0
end
end
end

View File

@ -1,13 +1,15 @@
- content_for :head do
= javascript_include_tag 'admin/custom_fields'
= javascript_include_tag 'admin/plugins/fancybox', 'admin/custom_fields'
= stylesheet_link_tag 'admin/plugins/fancybox', 'admin/box'
= f.inputs :name => :information do
= f.input :name
= f.input :slug
= f.input :description
= render 'admin/shared/custom_fields', :f => f, :collection_name => 'contents'
= render 'admin/custom_fields/index', :f => f, :collection_name => 'contents'
= f.foldable_inputs :name => :options, :class => 'switchable' do
= f.input :highlighted_field_name, :as => :select, :collection => options_for_highlighted_field(f.object, 'contents'), :include_blank => false
= f.input :order_by, :as => :select, :collection => options_for_order_by(f.object, 'contents'), :include_blank => false
= f.input :order_by, :as => :select, :collection => options_for_order_by(f.object, 'contents'), :include_blank => false

View File

@ -13,4 +13,6 @@
= render 'form', :f => form
= render 'admin/shared/form_actions', :back_url => admin_contents_url(@content_type.slug), :button_label => :update
= render 'admin/shared/form_actions', :back_url => admin_contents_url(@content_type.slug), :button_label => :update
= render 'admin/custom_fields/edit'

View File

@ -0,0 +1,8 @@
.box-wrapper
#edit-custom-field
%h2= t('.title')
= form_tag '#', :class => 'formtastic' do
= fields_for CustomFields::Field.new, :builder => Formtastic::SemanticFormHelper.builder do |g|
= g.inputs :name => :information do
= g.input :_alias

View File

@ -0,0 +1,53 @@
- collection_name = "#{collection_name.singularize}_custom_fields"
- custom_fields = f.object.send(collection_name.to_sym)
- ordered_custom_fields = f.object.send(:"ordered_#{collection_name}")
= f.foldable_inputs :name => :custom_fields, :class => 'editable-list fields off' do
- ordered_custom_fields.each do |field|
= f.fields_for collection_name.to_sym, 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.hidden_field :_alias, :class => 'alias'
= 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/pen.png'), '#edit-custom-field', :class => 'edit first'
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove', :confirm => t('admin.messages.confirm')
= f.fields_for collection_name.to_sym, custom_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.hidden_field :_alias, :class => 'alias'
= 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/pen.png'), '#edit-custom-field', :class => 'edit first'
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove', :confirm => t('admin.messages.confirm')
%button{ :class => 'button light add', :type => 'button' }
%span= t('admin.buttons.new_item')

View File

@ -10,6 +10,8 @@
= image_tag 'admin/form/icons/drag.png'
= g.hidden_field :position, :class => 'position'
= g.hidden_field :_alias, :class => 'alias'
= g.text_field :label
@ -22,7 +24,8 @@
&nbsp;
%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/pen.png'), '#', :class => 'edit first'
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove', :confirm => t('admin.messages.confirm')
= f.fields_for collection_name.to_sym, custom_fields.build(:label => 'field name'), :child_index => '-1' do |g|
%li{ :class => 'item template' }
@ -30,6 +33,8 @@
= image_tag 'admin/form/icons/drag.png'
= g.hidden_field :position, :class => 'position'
= g.hidden_field :_alias, :class => 'alias'
= g.text_field :label, :class => 'string label void'
@ -42,6 +47,9 @@
&nbsp;
%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/pen.png'), '#', :class => 'edit first'
= link_to image_tag('admin/form/icons/trash.png'), '#', :class => 'remove', :confirm => t('admin.messages.confirm')
%button{ :class => 'button light add', :type => 'button' }
%span= t('admin.buttons.new_item')
%span= t('admin.buttons.new_item')

View File

@ -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'

View File

@ -6,10 +6,10 @@
- if @image_assets.empty?
%p.no-items= t('.no_items')
- else
%ul.assets
= render 'asset', :asset => current_site.theme_assets.build, :edit => false
= render :partial => 'asset', :collection => @image_assets, :locals => { :per_row => 3, :edit => false }
%li.clear
%ul.assets
= render 'asset', :asset => current_site.theme_assets.build, :edit => false
= render :partial => 'asset', :collection => @image_assets, :locals => { :per_row => 3, :edit => false }
%li.clear

View File

@ -5,6 +5,7 @@ require File.expand_path('../boot', __FILE__)
require "action_controller/railtie"
require "action_mailer/railtie"
require "active_resource/railtie"
require "mongoid/railtie"
# Auto-require default libraries and those for the current Rails environment.
Bundler.require :default, Rails.env

View File

@ -1,20 +0,0 @@
defaults: &defaults
host: localhost
development:
<<: *defaults
database: locomotive_dev
test: &test
<<: *defaults
database: locomotive_test
production:
<<: *defaults
host: db.mongohq.com
username: user
password: pass
database: fanboy
cucumber:
<<: *test

View File

@ -30,16 +30,4 @@ Locomotive::Application.configure do
# Enable threaded mode
# config.threadsafe!
end
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
:enable_starttls_auto => true,
:address => "smtp.gmail.com",
:port => 587,
:domain => "nocoffee.fr",
:authentication => :plain,
:user_name => "didier@nocoffee.fr",
:password => "pepscou"
}
end

View File

@ -1,11 +1,5 @@
require 'mongoid'
File.open(File.join(Rails.root, 'config/database.yml'), 'r') do |f|
@settings = YAML.load(f)[Rails.env]
end
Mongoid::Config.instance.from_hash(@settings)
## various patches
module Mongoid #:nodoc:

View File

@ -33,9 +33,12 @@ en:
update: Update
custom_fields:
edit:
title: Editing custom field
kind:
string: Simple Input
text: Text
select: Select
sessions:
new:
@ -172,8 +175,7 @@ en:
title: '{{type}} &mdash; new item'
edit:
title: '{{type}} &mdash; editing item'
formtastic:
titles:
information: General information
@ -196,6 +198,10 @@ en:
source: File
edit:
source: Replace file
custom_fields:
custom_field:
_alias: Alias
hints:
page:
keywords: "Meta keywords used within the head tag of the page. They are separeted by an empty space. Required for SEO."

24
config/mongoid.yml Normal file
View File

@ -0,0 +1,24 @@
defaults: &defaults
host: localhost
# slaves:
# - host: slave1.local
# port: 27018
# - host: slave2.local
# port: 27019
development:
<<: *defaults
database: locomotive_dev
test:
<<: *defaults
database: locomotive_test
# set these environment variables on your prod server
production:
<<: *defaults
host: <%= ENV['MONGOID_HOST'] %>
port: <%= ENV['MONGOID_PORT'] %>
username: <%= ENV['MONGOID_USERNAME'] %>
password: <%= ENV['MONGOID_PASSWORD'] %>
database: <%= ENV['MONGOID_DATABASE'] %>

View File

@ -49,6 +49,7 @@ Rails.application.routes.draw do |map|
resources :contents, :path => "content_types/:slug/contents" do
put :sort, :on => :collection
end
end
# magic urls

View File

@ -6,13 +6,10 @@ x make an engine:
- deploy on Heroku
- refactoring: CustomFields::CustomField => CustomFields::Field
- new types for custom field
- file
- boolean
- date
- optimization custom_fields: use dynamic class for a collection instead of modifying the metaclass each time we build an item
x refactoring: CustomFields::CustomField => CustomFields::Field
- new custom field type
- category
x optimization custom_fields: use dynamic class for a collection instead of modifying the metaclass each time we build an item
BACKLOG:
@ -24,9 +21,16 @@ BACKLOG:
- theme assets: disable version if not image
- new custom field types
- file
- boolean
- date
- refactor slugify method (use parameterize + create a module)
BUGS:
- when assigning new layout, disabled parts show up :-( (js problem)
- password resets
- password resets (url is not handled correctly)
NICE TO HAVE:
- asset collections: custom resizing if image

View File

@ -32,4 +32,13 @@ class MiscFormBuilder < Formtastic::SemanticFormBuilder
end
end
def normalize_model_name(name)
if name =~ /(.+)\/(.+)/
[$1, $2]
else
super
end
end
end

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -19,15 +19,16 @@ $.growl.settings.dockCss = {
/* ___ codemirror ___ */
var addCodeMirrorEditor = function(type, el, parser) {
if (type == 'liquid') type = 'xml';
var parserfile = "parse" + type + ".js";
if (parser != undefined) parserfile = parser;
// if (type == 'liquid') type = 'xml';
if (parser != undefined) parserfile = parser;
var editor = CodeMirror.fromTextArea(el.attr('id'), {
height: "400px",
parserfile: parserfile,
stylesheet: [
"/stylesheets/admin/plugins/codemirror/csscolors.css",
"/stylesheets/admin/plugins/codemirror/xmlcolors.css",
"/stylesheets/admin/plugins/codemirror/javascriptcolors.css",
"/stylesheets/admin/plugins/codemirror/liquidcolors.css"],
path: "/javascripts/admin/plugins/codemirror/",

View File

@ -110,4 +110,25 @@ $(document).ready(function() {
axis: 'y',
update: refreshPosition
});
// edit in depth custom field
$('fieldset.fields li.item span.actions a.edit').click(function() {
var link = $(this);
$.fancybox({
titleShow: false,
content: $(link.attr('href')).parent().html(),
onComplete: function() {
$('#fancybox-wrap form').submit(function(e) {
$.fancybox.close();
e.preventDefault();
e.stopPropagation();
});
$('#fancybox-wrap #custom_fields_custom_field__alias').val(link.parent().prevAll('.alias').val());
},
onCleanup: function() {
link.parent().prevAll('.alias').val($('#fancybox-wrap #custom_fields_custom_field__alias').val());
}
})
});
});

View File

@ -7,7 +7,7 @@ $(document).ready(function() {
if (!slug.hasClass('filled')) {
setTimeout(function() {
slug.val(input.val().replace(/\s/g, '_').toLowerCase());
slug.val(input.val().replace(/[\s']/g, '_').toLowerCase());
}, 50);
}
});

View File

@ -71,6 +71,8 @@ var setupUploader = function() {
asset.removeClass('new-asset');
$('.asset-picker p.no-items').hide();
$('.asset-picker ul').scrollTo($('li.asset:last'), 400);
}
});

View File

@ -1,8 +1,10 @@
/* custom styles for fancybox */
div.asset-picker { width: 470px; position: relative; }
div.asset-picker .actions { position: absolute; right: 4px; top: 0px; }
div.asset-picker h2 {
/* ___ common ___ */
.box-wrapper { display: none; }
#fancybox-inner h2 {
border-bottom:1px dotted #BBBBBD;
color:#1E1F26;
font-size:1.1em;
@ -10,5 +12,28 @@ div.asset-picker h2 {
padding-bottom:10px;
}
#fancybox-inner form.formtastic legend span {
background-image: url("/images/admin/form/header-small.png");
width: 450px;
}
#fancybox-inner form.formtastic ol {
background-image: url("/images/admin/form/footer-small.png");
}
/* ___ asset picker ___ */
div.asset-picker { width: 470px; position: relative; }
div.asset-picker .actions { position: absolute; right: 4px; top: 0px; }
div.asset-picker p.no-items { background-image: url("/images/admin/list/none-small.png"); }
div.asset-picker ul { overflow: auto; height: 471px; }
div.asset-picker ul li.new-asset { display: none; }
div.asset-picker ul li.new-asset { display: none; }
/* ___ custom fields ___ */
#edit-custom-field {
width: 470px;
}

View File

@ -226,8 +226,9 @@ form.formtastic fieldset ol li.item span.actions {
position: absolute;
top: 7px;
right: 10px;
width: 16px;
width: 50px;
height: 16px;
text-align:right;
}
/* ___ editable-list (content type fields and validations) ___ */
@ -338,6 +339,10 @@ form.formtastic fieldset.editable-list ol li.template span.actions {
top: 10px;
}
form.formtastic fieldset.editable-list ol li.template span.actions a.edit {
display: none;
}
form.formtastic fieldset.editable-list ol li.template span.actions a.remove {
display: none;
}

View File

@ -20,7 +20,7 @@ describe AssetCollection do
context 'unit' do
before(:each) do
@field = CustomFields::CustomField.new(:kind => 'String')
@field = CustomFields::Field.new(:kind => 'String')
end
it 'should tell if it is a String' do
@ -75,6 +75,7 @@ describe AssetCollection do
context 'build and save' do
it 'should build asset' do
puts "___ TEST #1 ___"
asset = @collection.assets.build
lambda {
asset.description
@ -84,12 +85,14 @@ describe AssetCollection do
end
it 'should assign values to newly built asset' do
puts "___ TEST #2 ___"
asset = build_asset(@collection)
asset.description.should == 'Lorem ipsum'
asset.active.should == true
end
it 'should save asset' do
puts "___ TEST #3 ___"
asset = build_asset(@collection)
asset.save and @collection.reload
asset = @collection.assets.first
@ -98,6 +101,7 @@ describe AssetCollection do
end
it 'should not modify assets from another collection' do
puts "___ TEST #4 ___"
asset = build_asset(@collection)
asset.save and @collection.reload
new_collection = AssetCollection.new
@ -164,7 +168,7 @@ describe AssetCollection do
end
end
end
def build_asset(collection)

3
vendor/plugins/custom_fields/.rspec vendored Normal file
View File

@ -0,0 +1,3 @@
--colour
--format nested
--backtrace

11
vendor/plugins/custom_fields/Gemfile vendored Normal file
View File

@ -0,0 +1,11 @@
source "http://gemcutter.org"
gem "bson_ext", ">= 1.0.1"
gem "mongo_ext"
gem "mongoid", ">= 2.0.0.beta6"
gem "activesupport", ">= 3.0.0.beta3"
group :test do
gem 'rspec', '>= 2.0.0.beta.10'
gem 'mocha', :git => 'git://github.com/floehopper/mocha.git'
end

View File

@ -1,4 +1,4 @@
CustomField
CustomFields
===========
Introduction goes here.
@ -10,4 +10,4 @@ Example
Example goes here.
Copyright (c) 2010 [name of plugin creator], released under the MIT license
Copyright (c) 2010 [Didier Lafforgue], released under the MIT license

View File

@ -1,23 +1,33 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require "rubygems"
require "rake"
require "rake/rdoctask"
require "rspec"
require "rspec/core/rake_task"
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the custom_field plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.libs << 'test'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the custom_field plugin.'
desc 'Generate documentation for the custom_fields plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'CustomField'
rdoc.title = 'CustomFields'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end
end
# Spec::Rake::SpecTask.new(:rcov) do |spec|
# spec.libs << 'lib' << 'spec'
# spec.pattern = 'spec/**/*_spec.rb'
# spec.rcov = true
# end
Rspec::Core::RakeTask.new('spec:unit') do |spec|
spec.pattern = "spec/unit/**/*_spec.rb"
# spec.pattern = "spec/unit/custom_fields_for_spec.rb"
end
Rspec::Core::RakeTask.new('spec:integration') do |spec|
spec.pattern = "spec/integration/**/*_spec.rb"
end
task :spec => [:check_dependencies, 'spec:unit', 'spec:integration']
task :default => :spec

View File

@ -1 +0,0 @@
# Install hook code here

View File

@ -1,7 +1,15 @@
$:.unshift File.expand_path(File.dirname(__FILE__))
require 'active_support'
require 'custom_fields/extensions/mongoid/associations/proxy'
require 'custom_fields/extensions/mongoid/associations/has_many_related'
require 'custom_fields/extensions/mongoid/associations/embeds_many'
require 'custom_fields/extensions/mongoid/document'
require 'custom_fields/types/default'
require 'custom_fields/types/category'
require 'custom_fields/proxy_class_enabler'
require 'custom_fields/field'
require 'custom_fields/custom_fields_for'
module Mongoid

View File

@ -30,7 +30,7 @@ module CustomFields
class_eval <<-EOV
field :#{singular_name}_custom_fields_counter, :type => Integer, :default => 0
embeds_many :#{singular_name}_custom_fields, :class_name => "::CustomFields::CustomField"
embeds_many :#{singular_name}_custom_fields, :class_name => "::CustomFields::Field"
validates_associated :#{singular_name}_custom_fields
@ -39,6 +39,7 @@ module CustomFields
def ordered_#{singular_name}_custom_fields
self.#{singular_name}_custom_fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
end
EOV
end

View File

@ -2,6 +2,27 @@
module Mongoid #:nodoc:
module Associations #:nodoc:
class EmbedsMany < Proxy
def initialize_with_custom_fields(parent, options, target_array = nil)
if custom_fields?(parent, options.name)
options = options.clone # 2 parent instances should not share the exact same option instance
custom_fields = parent.send(:"ordered_#{custom_fields_association_name(options.name)}")
klass = options.klass.to_klass_with_custom_fields(custom_fields)
options.instance_eval <<-EOF
def klass=(klass); @klass = klass; end
def klass; @klass || class_name.constantize; end
EOF
options.klass = klass
end
initialize_without_custom_fields(parent, options, target_array)
end
alias_method_chain :initialize, :custom_fields
def build_with_custom_field_settings(attrs = {}, type = nil)
document = build_without_custom_field_settings(attrs, type)
@ -19,6 +40,5 @@ module Mongoid #:nodoc:
alias_method_chain :build, :custom_field_settings
end
end
end

View File

@ -0,0 +1,31 @@
# encoding: utf-8
module Mongoid #:nodoc:
module Associations #:nodoc:
# Represents an relational one-to-many association with an object in a
# separate collection or database.
class HasManyRelated < Proxy
def initialize_with_custom_fields(parent, options, target_array = nil)
if custom_fields?(parent, options.name)
options = options.clone # 2 parent instances should not share the exact same option instance
custom_fields = parent.send(:"ordered_#{custom_fields_association_name(options.name)}")
klass = options.klass.to_klass_with_custom_fields(custom_fields)
options.instance_eval <<-EOF
def klass=(klass); @klass = klass; end
def klass; @klass || class_name.constantize; end
EOF
options.klass = klass
end
initialize_without_custom_fields(parent, options, target_array)
end
alias_method_chain :initialize, :custom_fields
end
end
end

View File

@ -0,0 +1,16 @@
# encoding: utf-8
module Mongoid #:nodoc
module Associations #:nodoc
class Proxy #:nodoc
def custom_fields_association_name(association_name)
"#{association_name.to_s.singularize}_custom_fields".to_sym
end
def custom_fields?(object, association_name)
object.respond_to?(custom_fields_association_name(association_name))
end
end
end
end

View File

@ -2,34 +2,34 @@
module Mongoid #:nodoc:
module Document
module InstanceMethods
def parentize_with_custom_fields(object, association_name)
parentize_without_custom_fields(object, association_name)
if self.custom_fields?(object, association_name)
# puts "[parentize_with_custom_fields] association_name = #{association_name} / #{self.custom_fields_association_name(association_name)}"
object.send(self.custom_fields_association_name(association_name)).each do |field|
field.apply(self, association_name)
end
self.instance_eval <<-EOV
def custom_fields
fields = self._parent.send(:#{self.custom_fields_association_name(association_name)})
fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
end
EOV
end
end
alias_method_chain :parentize, :custom_fields
def custom_fields_association_name(association_name)
"#{association_name.to_s.singularize}_custom_fields".to_sym
end
def custom_fields?(object, association_name)
object.respond_to?(custom_fields_association_name(association_name)) &&
object.associations[association_name]
end
# def parentize_with_custom_fields(object, association_name)
# parentize_without_custom_fields(object, association_name)
#
# if self.custom_fields?(object, association_name)
# # puts "[parentize_with_custom_fields] association_name = #{association_name} / #{self.custom_fields_association_name(association_name)}"
# object.send(self.custom_fields_association_name(association_name)).each do |field|
# field.apply(self)
# end
#
# self.instance_eval <<-EOV
# def custom_fields
# fields = self._parent.send(:#{self.custom_fields_association_name(association_name)})
# fields.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
# end
# EOV
# end
# end
#
# alias_method_chain :parentize, :custom_fields
#
# def custom_fields_association_name(association_name)
# "#{association_name.to_s.singularize}_custom_fields".to_sym
# end
#
# def custom_fields?(object, association_name)
# object.respond_to?(custom_fields_association_name(association_name)) &&
# object.associations[association_name]
# end
end
end
end

View File

@ -1,8 +1,12 @@
module CustomFields
class CustomField
include Mongoid::Document
include Mongoid::Timestamps
class Field
include ::Mongoid::Document
include ::Mongoid::Timestamps
# types ##
include Types::Default
include Types::Category
## fields ##
field :label, :type => String
@ -17,7 +21,7 @@ module CustomFields
## methods ##
%w{String Text Email Boolean Date File}.each do |kind|
%w{String Text Category}.each do |kind|
define_method "#{kind.downcase}?" do
self.kind == kind
end
@ -25,27 +29,42 @@ module CustomFields
def field_type
case self.kind
when 'String', 'Text', 'Email' then String
when 'String', 'Text', 'Category' then String
else
self.kind.constantize
end
end
def apply(object, association_name)
def apply(klass)
return unless self.valid?
# trick mongoid
object.class_eval { def meta; (class << self; self; end); end }
object.meta.fields = object.fields.clone
object.meta.send(:define_method, :fields) { self.meta.fields }
klass.field self._name, :type => self.field_type
object.meta.field self._name, :type => self.field_type
object.class_eval <<-EOF
alias :#{self.safe_alias} :#{self._name}
alias :#{self.safe_alias}= :#{self._name}=
EOF
case self.kind
when 'Category'
apply_category_type(klass)
else
apply_default_type(klass)
end
end
# def apply_to_object(object)
# return unless self.valid?
#
# # trick mongoid: fields are now on a the singleton class level also called metaclass
# self.singleton_class.fields = self.fields.clone
# self.singleton_class.send(:define_method, :fields) { self.singleton_class.fields }
#
# object.singleton_class.field self._name, :type => self.field_type
#
# case self.kind
# when 'Category'
# apply_category_type(object)
# else
# apply_default_type(object)
# end
# end
def safe_alias
self.set_alias
self._alias
@ -66,8 +85,7 @@ module CustomFields
def set_alias
return if self.label.blank? && self._alias.blank?
self._alias ||= self.label.clone
self._alias.slugify!(:downcase => true, :underscore => true)
self._alias = (self._alias || self.label).parameterize('_').downcase
end
def increment_counter!

View File

@ -0,0 +1,37 @@
module CustomFields
module ProxyClassEnabler
extend ActiveSupport::Concern
included do
cattr_accessor :klass_with_custom_fields
def self.to_klass_with_custom_fields(fields)
return klass_with_custom_fields unless klass_with_custom_fields.nil?
klass = Class.new(self)
klass.class_eval <<-EOF
cattr_accessor :custom_fields
def self.model_name
@_model_name ||= ActiveModel::Name.new(self.superclass)
end
def custom_fields
self.class.custom_fields
end
EOF
klass.hereditary = false
klass.custom_fields = fields
[*fields].each { |field| field.apply(klass) }
klass_with_custom_fields = klass
end
end
end
end

View File

@ -0,0 +1,52 @@
module CustomFields
module Types
module Category
extend ActiveSupport::Concern
included do
embeds_many :category_items, :class_name => 'CustomFields::Types::Category::Item'
end
module InstanceMethods
def ordered_category_items
self.category_items.sort { |a, b| (a.position || 0) <=> (b.position || 0) }
end
def apply_category_type(klass)
klass.cattr_accessor :"#{self.safe_alias}_items"
klass.send("#{self.safe_alias}_items=", self.category_items)
klass.class_eval <<-EOF
def self.#{self.safe_alias}_names
#{self.safe_alias}_items.collect(&:name)
end
def #{self.safe_alias}=(name)
category_id = self.class.#{self.safe_alias}_items.where(:name => name).first._id rescue nil
write_attribute(:#{self._name}, category_id)
end
def #{self.safe_alias}
category_id = read_attribute(:#{self._name})
self.class.#{self.safe_alias}_items.find(category_id).name rescue nil
end
EOF
end
end
class Item
include Mongoid::Document
field :name
field :position, :type => Integer, :default => 0
embedded_in :custom_field, :inverse_of => :category_items
end
end
end
end

View File

@ -0,0 +1,18 @@
module CustomFields
module Types
module Default
extend ActiveSupport::Concern
module InstanceMethods
def apply_default_type(klass)
klass.class_eval <<-EOF
alias :#{self.safe_alias} :#{self._name}
alias :#{self.safe_alias}= :#{self._name}=
EOF
end
end
end
end
end

View File

@ -0,0 +1,10 @@
class Person
include Mongoid::Document
include CustomFields::ProxyClassEnabler
field :full_name
embedded_in :project, :inverse_of => :people
end

View File

@ -0,0 +1,16 @@
class Project
include Mongoid::Document
include CustomFields::ProxyClassEnabler
include CustomFields::CustomFieldsFor
field :name
field :description
has_many_related :people
embeds_many :tasks
custom_fields_for :people
custom_fields_for :tasks
end

View File

@ -0,0 +1,10 @@
class Task
include Mongoid::Document
include CustomFields::ProxyClassEnabler
field :title
embedded_in :project, :inverse_of => :tasks
end

View File

@ -0,0 +1,30 @@
$LOAD_PATH.unshift(File.dirname(__FILE__))
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
MODELS = File.join(File.dirname(__FILE__), "models")
$LOAD_PATH.unshift(MODELS)
require 'rubygems'
require 'bundler'
Bundler.setup
Bundler.require
require 'mongoid'
require 'mocha'
require 'rspec'
require 'custom_fields'
Dir[ File.join(MODELS, "*.rb") ].sort.each { |file| require File.basename(file) }
Mongoid.configure do |config|
name = "custom_fields_test"
host = "localhost"
config.master = Mongo::Connection.new.db(name)
end
Rspec.configure do |config|
config.mock_with :mocha
config.after :suite do
Mongoid.master.collections.each(&:drop)
end
end

View File

@ -0,0 +1,42 @@
require 'spec_helper'
describe CustomFields::Field do
it 'is initialized' do
lambda { CustomFields::Field.new }.should_not raise_error
end
context '#mongoid' do
before(:each) do
@field = CustomFields::Field.new(:label => 'manager', :_name => 'field_1', :kind => 'String', :_alias => 'manager')
@field.stubs(:valid?).returns(true)
@project = Project.to_klass_with_custom_fields(@field).new
end
it 'is added to the list of mongoid fields' do
@project.fields['field_1'].should_not be_nil
end
end
context 'on target class' do
before(:each) do
@field = CustomFields::Field.new(:label => 'manager', :_name => 'field_1', :kind => 'String', :_alias => 'manager')
@field.stubs(:valid?).returns(true)
@project = Project.to_klass_with_custom_fields(@field).new
end
it 'has a new field' do
@project.respond_to?(:manager).should be_true
end
it 'sets / retrieves a value' do
@project.manager = 'Mickael Scott'
@project.manager.should == 'Mickael Scott'
end
end
end

View File

@ -0,0 +1,85 @@
require 'spec_helper'
describe CustomFields::CustomFieldsFor do
context 'with embedded collection' do
context '#association' do
before(:each) do
@project = Project.new
end
it 'has custom fields for embedded collection' do
@project.respond_to?(:task_custom_fields).should be_true
end
end
context '#building' do
before(:each) do
@project = Project.new
@project.task_custom_fields.build :label => 'Short summary', :_alias => 'summary', :kind => 'String'
@task = @project.tasks.build
end
it 'returns a new document whose Class is different from the original one' do
@task.class.should_not == Task
end
it 'returns a new document with custom field' do
@project.tasks.build
@project.tasks.build
@task.respond_to?(:summary).should be_true
end
it 'sets/gets custom attributes' do
@task.summary = 'Lorem ipsum...'
@task.summary.should == 'Lorem ipsum...'
end
end
end
context 'with related collection' do
context '#association' do
before(:each) do
@project = Project.new
end
it 'has custom fields for related collections' do
@project.respond_to?(:person_custom_fields).should be_true
end
end
context '#building' do
before(:each) do
@project = Project.new
@project.person_custom_fields.build :label => 'Position in the project', :_alias => 'position', :kind => 'String'
@person = @project.people.build
end
it 'returns a new document whose Class is different from the original one' do
@person.class.should_not == Task
end
it 'returns a new document with custom field' do
@person.respond_to?(:position).should be_true
end
it 'sets/gets custom attributes' do
@person.position = 'Designer'
@person.position.should == 'Designer'
end
end
end
end

View File

@ -0,0 +1,25 @@
require 'spec_helper'
describe CustomFields::ProxyClassEnabler do
context '#proxy klass' do
before(:each) do
@klass = Task.to_klass_with_custom_fields([])
end
it 'does not be flagged as a inherited document' do
@klass.new.hereditary?.should be_false
end
it 'has a list of custom fields' do
@klass.custom_fields.should == []
end
it 'has the exact same model name than its parent' do
@klass.model_name.should == 'Task'
end
end
end

View File

@ -0,0 +1,58 @@
require 'spec_helper'
describe CustomFields::Types::Category do
context 'on field class' do
before(:each) do
@field = CustomFields::Field.new
end
it 'has the category items field' do
@field.respond_to?(:category_items).should be_true
end
it 'has the apply method used for the target object' do
@field.respond_to?(:apply_category_type).should be_true
end
end
context 'on target class' do
before(:each) do
@project = build_project_with_category
end
it 'has getter/setter' do
@project.respond_to?(:global_category).should be_true
@project.respond_to?(:global_category=).should be_true
end
it 'has the values of this category' do
@project.class.global_category_names.should == %w{Development Design Maintenance}
end
it 'sets the category from a name' do
@project.global_category = 'Design'
@project.global_category.should == 'Design'
@project.field_1.should_not be_nil
end
end
def build_project_with_category
field = build_category
Project.to_klass_with_custom_fields(field).new
end
def build_category
field = CustomFields::Field.new(:label => 'global_category', :_name => 'field_1', :kind => 'Category')
field.stubs(:valid?).returns(true)
field.category_items.build :name => 'Development'
field.category_items.build :name => 'Design'
field.category_items.build :name => 'Maintenance'
field
end
end