I need to create an enumeration that I will need to initialize from a value from the querystring.
Example of what I have and what I need to do:
class UserType
NONE = 0
MEMBER = 1
ADMIN = 2
SUPER = 3
end
Now in my querystring I will have:
/users/load_by_type?type=2
Now in my controller I will get the value 2 from the querystring, I then need to have a UserType object which has the value 'MEMBER'.
How can I do this?
If my class isn't really a good enumeration hack, please advise.
How about something like this.
require 'active_record'
# set up db
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
# define schema
ActiveRecord::Schema.define do
suppress_messages do
create_table :users do |t|
t.string :name
t.string :role
end
end
end
# define class
class User < ActiveRecord::Base
Roles = %w[none member admin super].map(&:freeze)
validates_inclusion_of :role, in: Roles
end
# specification
describe User do
before { User.delete_all }
let(:valid_role) { User::Roles.first }
let(:invalid_role) { valid_role.reverse }
it 'is valid if its role is in the Roles list' do
User.new.should_not be_valid
User.new(role: valid_role).should be_valid
User.new(role: invalid_role).should_not be_valid
end
let(:role) { User::Roles.first }
let(:other_role) { User::Roles.last }
it 'can find users by role' do
user_with_role = User.create! role: role
user_with_other_role = User.create! role: other_role
User.find_all_by_role(role).should == [user_with_role]
end
end
It does have the disadvantage of using an entire string (255 chars) for the enumeration method, but it also has the advantage of readability and ease of use (it would probably come in as "/users/load_by_role?role=admin"). Besides, if at some point it winds up costing too much, it should be easy to update to use a small integer.
I think I'd rather use hashes for this kind of thing, but just for fun:
class Foo
BAR = 1
STAN = 2
class << self
def [](digit)
constants.find { |const| const_get(const) == digit }
end
end
end
puts Foo[1] # BAR
puts Foo[2] # STAN
Related
I am using an enum in Rails and PostgreSQL. In my model tests I usually verify that my Rails validations are backed by database constraints where appropriate (e.g. presence: true in Model and null: false in DB). I do this by deliberately making the model invalid, attempting to save it without validations and making sure it raises a ActiveRecord::StatementInvalid error.
How do I test a PostgreSQL enum from MiniTest? My usual approach isn't working as everything I try to do to set my ActiveRecord model to an invalid enum value raises an ArgumentError, even using write_attribute() directly.
Is there a way to deliberately bypass the enum restrictions in Rails? Do I need to drop down out of ActiveRecord and send an AREL or SQL query direct to the database? Is there some other approach?
# Model
class SisRecord < ApplicationRecord
enum record_type: {
student: "student",
staff: "staff",
contact: "contact"
}
validates :record_type, presence: true
end
# Migration
class CreateSisRecords < ActiveRecord::Migration[7.0]
def change
create_enum :sis_record_type, %w(student staff contact)
create_table :sis_records do |t|
t.enum :record_type, enum_type: :sis_record_type, null: false
t.timestamps
end
end
end
# Test
require "test_helper"
class SisRecordTest < ActiveSupport::TestCase
test "a record_type is required" do
record = sis_records(:valid_sis_record)
record.record_type = nil
assert_not record.save, "Saved the SIS Record without a record type"
end
test "a record_type is required by the database too" do
record = sis_records(:valid_sis_record)
record.record_type = nil
assert_raises(ActiveRecord::StatementInvalid) {
record.save(validate: false)
}
end
test "record_type is restricted to accepted values" do
accepted_values = %w(student staff contact)
record = sis_records(:valid_sis_record)
assert_nothing_raised {
record.record_type = accepted_values.sample
}
assert_raises(ArgumentError) {
record.record_type = "something else"
}
end
test "record_type is restricted to accepted values by the database too" do
accepted_values = %w(student staff contact)
record = sis_records(:valid_sis_record)
record.record_type = accepted_values.sample
assert record.save, "Record didn't save despite accepted type value '#{record.record_type}'"
record.write_attribute(:record_type, "nonsense") ### <-- ArgumentError
assert_raises(ActiveRecord::StatementInvalid) {
record.save(validate: false)
}
end
end
I have an answer to my own question, but I'm still open to better answers.
I found a comment on a gist that showed how to fairly simply insert a record with Arel so for now I am using this approach:
# Just the test in question
test "record_type is restricted to accepted values by the database too" do
accepted_values = %w(student staff contact)
table = Arel::Table.new(:sis_records)
manager = Arel::InsertManager.new
manager.insert [
[table[:record_type], accepted_values.sample],
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
assert_nothing_raised {
SisRecord.connection.insert(manager.to_sql)
}
manager.insert [
[table[:record_type], "other type"],
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
assert_raises(ActiveRecord::StatementInvalid) {
SisRecord.connection.insert(manager.to_sql)
}
end
created_at and updated_at are required fields so we have to add a value for those.
In my real case (not the simplified version I posted above), SisRecord belongs to Person so I had to provide a valid person ID (UUID) too. I did this by grabbing an ID from my people fixtures:
manager.insert [
[table[:record_type], "other type"],
[table[:person_id], people(:valid_person).id], # <--------
[table[:created_at], Time.now],
[table[:updated_at], Time.now],
]
I created a concern in model
module Employee
extend ActiveSupport::Concern
included do
def job
job_hash = {
'0': 'A',
'1': 'B',
'2': 'C'
}.freeze
job_hash[:"#{#job}"]
end
end
I have model
class Company < ActiveRecord::Base
self.table_name = 'Jobs'
include Employee
end
Jobs table has column type that has values 0, 1, 2
How can i map the values from hash (0,1,2) in concern with column type and return the string value associated with it? so if the user type is 1 then return B
Thanks
You could just decouple the #job method and use it like this (if I'm understanding you correctly):
module Employee
extend ActiveSupport::Concern
included do
def job
job_mapping[:"#{#job}"]
end
def job_mapping
{
'0' => 'A',
'1' => 'B',
'2' => 'C'
}
end
def job_name
job_mapping[type.to_s]
end
end
end
company = Company.find(some_id)
job_name = company.job_name
On an unrelated note, freezeing something that is in an instance method is a code smell, you probably want to promote that to a constant if possible.
I'm trying to build an application on RoR that uses MongoDB via Mongoid for the main objects but has a like and dislike process using Redis via Opinions https://github.com/leehambley/opinions/.
It sort of works but when I run the methods on my objects I just get an error "undefined method `like_by'" where I think the methods are supposed to be autogenerated.
My model looks like:
class Punchline
include Mongoid::Document
include Opinions::Pollable
opinions :like, :dislike
field :key, type: String
field :text, type: String
field :won, type: Boolean
field :created, type: Time, default: ->{ Time.now }
field :score, type: Integer
index({ key: 1 }, { unique: true, name: "key_index" })
belongs_to :user
embedded_in :joke
end
and I run:
user = User.find(session[:userid])
#joke.punchlines.sample.like_by(user);
But it fails with the undefined method error :(
Do I need to initialize Opinions somewhere beyond
/config/initializers/opinions.rb
Opinions.backend = Opinions::RedisBackend.new
Redis.new(:host => 'localhost', :port => 6379)
So, it turns out that Opinions doesn't really work. Why is a bit beyond my two weeks with Rails :)
Anyway, it turns out that this is really easy to do by hand anyway especially as I only had a like and dislike to handle.
I used a Redis sorted set which allows a unique key - value pair with a score. I used a score of +1 or -1 to denote like or dislike and then encoded the key to represent the liked object and the value to be the user id.
This looked like:
def like(user)
$redis.zadd('joke:'+self.key+':likes', 1, user._id)
end
def dislike(user)
$redis.zadd('joke:'+self.key+':likes', -1, user._id)
end
def numLikes
res = $redis.zrangebyscore('joke:'+self.key+':likes',1,1);
return res.count
end
def numDislikes
res = $redis.zrangebyscore('joke:'+self.key+':likes',-1,-1);
return res.count
end
def likedBy(user)
res = $redis.zscore('joke:'+self.key+':likes',user._id)
return (res == 1)
end
def dislikedBy(user)
res = $redis.zscore('joke:'+self.key+':likes',user._id)
return (res == -1)
end
In one of my models I have defined equality to also work with strings and symbols. A role is equal to another role (or string or symbol), if its name attribute is the same:
class Role
def == other
other_name = case other
when Role then other.name
when String, Symbol then other.to_s
end
name == other_name
end
end
The equality checking works correct:
role = Role.create name: 'admin'
role == 'admin' # => true
role == :admin # => true
But when I use the Role model in a has_many relationship, in the collection I get, include? does not recognized this equality:
user = User.create
user.roles << role
User.roles.include? role # => true
User.roles.include? 'admin' # => false
User.roles.include? :admin # => false
In order to make this work, I have to explicitly convert this to an array:
User.roles.to_a.include? 'admin' # => true
User.roles.to_a.include? :admin # => true
So apparently Rails overrides the include? method in the array returned by user.roles. This sucks and is contrary to rubys specification of Enumerable#include? (which explicitly states, that "Equailty is tested using =="). This is not true for the array I get from user.roles. == is never even called.
Where is this modified behavior of include? specified?
Is there another way to test for inclusion that I missed? Or do I have to use to_a or an actual instance of Role everytime?
You are not implementing your equality operator correctly. It should be:
def == other
other_name = case other
when Role then other.name
when String, Symbol then other.to_s
end
name == other_name
end
I have two types of classes:
BaseUser < ActiveRecord::Base
and
User < BaseUser
which acts_as_authentic using Authlogic's authentication system. This inheritance is implemented using Single Table Inheritance
If a new user registers, I register him as a User. However, if I already have a BaseUser with the same email, I'd like to change that BaseUser to a User in the database without simply copying all the data over to the User from the BaseUser and creating a new User (i.e. with a new id). Is this possible? Thanks.
Steve's answer works but since the instance is of class BaseUser when save is called, validations and callbacks defined in User will not run. You'll probably want to convert the instance using the becomes method:
user = BaseUser.where(email: "user#example.com").first_or_initialize
user = user.becomes(User) # convert to instance from BaseUser to User
user.type = "User"
user.save!
You can just set the type field to 'User' and save the record. The in-memory object will still show as a BaseUser but the next time you reload the in-memory object will be a User
>> b=BaseUser.new
>> b.class # = BaseUser
# Set the Type. In-Memory object is still a BaseUser
>> b.type='User'
>> b.class # = BaseUser
>> b.save
# Retrieve the records through both models (Each has the same class)
>> User.find(1).class # = User
>> BaseUser.find(1).class # User
Based on the other answers, I expected this to work in Rails 4.1:
def update
#company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != #company.type && Company::COMPANY_TYPES.include?(new_type)
#company.becomes!(new_type.constantize)
#company.type = new_type
#company.save!
end
#company.update(company_params)
respond_with(#company)
end
It did not, as the type change would not persist. Instead, I went with this less elegant approach, which works correctly:
def update
#company = Company.find(params[:id])
# This separate step is required to change Single Table Inheritance types
new_type = params[:company][:type]
if new_type != #company.type && Company::COMPANY_TYPES.include?(new_type)
#company.update_column :type, new_type
end
#company.update(company_params)
respond_with(#company)
end
And here are the controller tests I used to confirm the solution:
describe 'Single Table Inheritance (STI)' do
class String
def articleize
%w(a e i o u).include?(self[0].to_s.downcase) ? "an #{self}" : "a #{self}"
end
end
Company::COMPANY_TYPES.each do |sti_type|
it "a newly assigned Company of type #{sti_type} " \
"should be #{sti_type.articleize}" do
post :create, { company: attributes_for(:company, type: sti_type) },
valid_session
expect(assigns(:company)).to be_a(sti_type.constantize)
end
end
Company::COMPANY_TYPES.each_index do |i|
sti_type, next_sti_type = Company::COMPANY_TYPES[i - 1],
Company::COMPANY_TYPES[i]
it "#{sti_type.articleize} changed to type #{next_sti_type} " \
"should be #{next_sti_type.articleize}" do
company = Company.create! attributes_for(:company, type: sti_type)
put :update, { id: company.to_param, company: { type: next_sti_type } },
valid_session
reloaded_company = Company.find(company.to_param)
expect(reloaded_company).to be_a(next_sti_type.constantize)
end
end
end