Skip to content

anga/extend_at

Repository files navigation

Extend at

This gem allows you to extend models without migrations: This way you can, i.e., develop your own content types, like in Drupal.

For example, if you want to create an administration panel to add columns to a model, for example, you are working on a CMS, and you want to create a "content type" and you need to set the "columns" but you don't want to migrate the database, then, you can see this little tutorial.

Status:

Build Status

Installation

gem install extend_at

Rails 3

Add in your Gemfile:

gem 'extend_at'

After that, you need execute:

rails generate extend_at:install

This will generate one migration with all necessary tables. Now you need migrate your database.

rake db:migrate

Usage

You don't need an extra column in your model. Only you need is put next code in your model.

extend_at :extra

For example:

class User < ActiveRecord::Base
  extend_at :extra
end

Now you can create extra attributes:

user.extra.private_photos = true
user.extra.subscribe_to_news = false
user.extra.profile_description = ''
user.save

This is the same:

user.extra_private_photos = true
user.extra_subscribe_to_news = false
user.extra_profile_description = ''
user.save

Or:

user[:extra_private_photos] = true
user[:extra_subscribe_to_news] = false
user[:extra_profile_description] = ''
user.save

Columns configuration

You can configure each column.

Set column type

You can set the colum's type.

class User < ActiveRecord::Base
  extend_at :extra, :columns => {
    :private_photos => {
      :type => :boolean
    }, :age => {
      :type => :get_type
    }, :profile_description => {
      :type => lambda {
        String
      }
    }, :last_loggin => {
      :type => Time.now.class
    }, :subscribe_to_rss => :get_rss_config
  }

  protected
  def get_type
    Fixnum
  end

  def get_rss_config
    {
      :type => :boolean
    }
  end
end
Valid types

Valid symbols:

  • :any
  • :binary
  • :boolean
  • :date
  • :datetime
  • :decimal
  • :float
  • :integer
  • :string
  • :text
  • :time
  • :timestamp

But you can use classes.

  • Float: :any
  • Fixnum: :integer
  • String: :text
  • Time: :timestamp
  • Date: :datetime

Else, return :any

Set default value

class User < ActiveRecord::Base
  extend_at :extra, :columns => {
    :private_photos => {
      :type => :boolean,
      :default => true
    }, :age => {
      :type => :get_type,
      :default => 1
    }, :profile_description => {
      :type => lambda {
        String
      },
      :default => :get_default_profile_description
    }, :last_loggin => {
      :type => Time.now.class,
      :default => lambda {
        self.created_at.time
      }
    }, :subscribe_to_rss => :get_rss_config
  }

  protected
  def get_type
    Fixnum
  end

  def get_rss_config
    {
      :type => :boolean,
      :default => true
    }
  end

  def get_default_profile_description
    Description.where(:user_id => self.id).default
  end
end

Set validation

class User < ActiveRecord::Base
  extend_at :extra, :columns => {
    :private_photos => {
      :type => :boolean,
      :default => true
    }, :age => {
      :type => :get_type,
      :default => 1,
      :validate => lambda {
        |age|
        errors.add :extra_age, "Are you Matusalen?" if age > 150
        errors.add :extra_age, "Are you a fetus?" if age <= 0
      }
    }, :profile_description => {
      :type => lambda {
        String
      },
      :default => :get_default_profile_description,
      :lambda => :must_not_use_strong_language
    }, :last_loggin => {
      :type => Time.now.class,
      :default => lambda {
        self.created_at.time
      },
      :validate => lambda {
        |time|
        errors.add :extra_last_loggin, "You can't loggin on the future" if time > Time.now
      }
    }, :subscribe_to_rss => :get_rss_config
  }

  protected
  STRONG_WORD = [
    #...
  ]
  
  def get_type
    Fixnum
  end

  def get_rss_config
    {
      :type => :boolean,
      :default => true
    }
  end

  def get_default_profile_description
    Description.where(:user_id => self.id).default
  end

  def must_not_use_strong_language(desc)
    errors.add :cofig_profile_description, "You must not use strong language" if desc =~ /(#{STRONG_WORD.join('|')})/
  end
end

Static columns

If you like to restrict the existent extended columns, you should use :static => true

class User < ActiveRecord::Base
  extend_at :extra, :columns => {
    :private_photos => {
      :type => :boolean,
      :default => true
    }
  }, :static => true
end

Now, User.extra only accept private_photos column

Scopes

You can use scope like:

User.extra_last_loggin_gt_eq(1.week.ago).extra_age_gt_eq(18).where(:column => "value").all

Valid scopes:

<extention>_<column_name>_<comparation>

Comparations:

  • lt
  • lt_eq
  • eq
  • gt_eq
  • gt
  • match

Belongs to

If you like to add a belongs_to relationship, you can do it in this way:

# app/models/toolbox.rb
class Toolbox
end

# app/model/tool.rb
class Tool
  extend_at extra, columns => {}, :belongs_to => :toolbox
end

:belongs_to parametter accept

  • One name

    :belongs_to => :toolbox

  • Array of names

    :belongs_to => [:toolbox, :owner]

  • Hash

    :belongs_to => {:onwer => {:class_name => "User"}}

For now, hash only accept

  • class_name
  • polymorphic
  • foreign_key

Note, this new feature is under heavy development, use it under your own risk.

Integration in the views

If you like to use some configuration variable in your views you only need put the name of the input like :extra_name, for example:

<% form_for(@user) do |f| %>
  ...
  <div class="field">
    <%= f.label :extra_private_photos %><br />
    <%= f.check_box :extra_private_photos %>
  </div>
  ...
<% end %>

More

For more documentation go to wiki.

Tips

If you like to do something more dynamic, like create columns and validations depending of some model or configuration, then you can do something like this:

class User < ActiveRecord::Base
  extend_at :extra, :columns => :get_columns
  serialize :columns_name

  protected
  def get_columns
    columns = {}
    columns_name.each do |name|
      config = ColumConfig.where(:user_id => self.id, :column => name).first
      columns[name.to_sym] = {
        :type => eval(config.class_type),
        :default => config.default_value,
        :validate => get_validation(config)
      }
    end
    
    columns
  end

  # Accept a name of a validation and return the Proc with the validation code
  def get_validation(validation_type)
    # ...
  end
end

How works?

serialize :columns_name

This make columns_name column work like YAML serialized object. In this case, is used to sotore an array of names of each column name. (See ActiveRecord::Base)

extend_at :extra, :columns => :get_columns

This line will use the function get_columns to get the information about each column dynamically. This function returns a hash with the information about each column.

columns_name.each do |name|
    #...
end

Iterate through each column stored in the column columns_name.

config = ColumConfig.where(:user_id => self.id, :column => name).first

Search the column configuration stored in a separated model. By this way, we can configura each column easily, we can create a view to create columns and configure it easily.

columns[name.to_sym] = {
    :type => eval(config.class_type),
    :default => config.default_value,
    :validate => get_validation(config)
  }

This lines configure the column.

:type => eval(config.class_type),

The model ColumConfig have a string column named class_type, can be ":integer" or "Fixnum".

:default => config.default_value,

The model ColumConfig have a serialized column named default_valuee, in this way we can sotre integer values, boolean, strings or datetime and time values without problems.

:validate => get_validation(config.validation)

This line execute the function get_validation to get a Proc with the validation code. This function can use a case/when for select the correct function or use the data of the model config to create a function.

columns

Finally we return the colums configuration.

Bugs, recomendation, etc

If you found a bug, create an issue. If you have a recomendation, idea, etc., create a request or fork the project.

License

This gem is under MIT license.

About

Create dynamic fields to extend rails models without migrations (like content types in Drupal)

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages