Make friendly_id scope play nice with subclassed ActiveRecord model - ruby

I have a subclassed ActiveRecord model which uses a separate table to store records and friendly_id (4.1.0.beta.1) to generate slugs. Problem is friendly_id is using the parent class's table to check for existing slugs, instead of using the child table. Basically I'd like friendly_id to scope its checks to the right table.
Example:
class Parent
friendly_id :name, :use => :slugged
end
class Child < Parent
self.table_name = 'children'
end
Parent.create(name: 'hello').slug
> 'hello'
Child.create(name: 'hello').slug
> 'hello--2'
I want friendly_id to generate the 'hello' slug for the second create, because there are no records in the children table with that slug. Is there a way to configure or monkey patch the class friendly id uses for its queries?
EDIT: added friendly_id version for future reference

I'm posting my own solution to this problem, just in case someone is having the same problem. I should reiterate that this problem was found on version 4.1.0.beta.1 of the friendly_id gem (which at the time was the most recent version), so this issue may not occur any more.
To solve this problem, I basically configured slug_generator_class to use my own class, so I could monkey patch the culprit method.
In my model:
friendly_id do |config|
config.slug_generator_class = SubclassScopableSlugGenerator
end
In an initializer, I overrode the FriendlyId::SlugGenerator.conflicts method so I could access the sluggable_class var:
# Lets a non-STI subclass of a FriendlyId parent (i.e. a subclass with its
# own dedicated table) have independent slug uniqueness.
class SubclassScopableSlugGenerator < FriendlyId::SlugGenerator
private
def conflicts
# this is the only line we're actually changing
sluggable_class = friendly_id_config.model_class
pkey = sluggable_class.primary_key
value = sluggable.send pkey
base = "#{column} = ? OR #{column} LIKE ?"
# Awful hack for SQLite3, which does not pick up '\' as the escape character without this.
base << "ESCAPE '\\'" if sluggable.connection.adapter_name =~ /sqlite/i
scope = sluggable_class.unscoped.where(base, normalized, wildcard)
scope = scope.where("#{pkey} <> ?", value) unless sluggable.new_record?
length_command = "LENGTH"
length_command = "LEN" if sluggable.connection.adapter_name =~ /sqlserver/i
scope = scope.order("#{length_command}(#{column}) DESC, #{column} DESC")
end
end

Related

Paper Trail Gem: How to use a custom item_type for specific model

Is there an option to configure the item_type for a version? I have a class Post, and the default item_type for that would be Post; is there an option to configure that be Foo?
UPDATE with example:
class Post < ActiveRecord::Base
has_paper_trail
end
post = Post.create # this creates a new post along with a version.
version = post.versions.first
version.item_type = 'Post' # Item type here is the name of the base model 'Post'.
Is there an option to configure the item_type for a version? I have a class Post, and the default item_type for that would be Post; is there an option to configure that be Foo?
No. When you call has_paper_trail, it adds a polymorphic association named item. Therefore, PaperTrail does not control the database column item_type, ActiveRecord does.
Here is the definition of the has_many association:
has_many(
versions_association_name, # Usually "versions"
-> { order(model.timestamp_sort_order) }, # Usually "created_at"
class_name: version_class_name, # Usually "PaperTrail::Version"
as: :item # Specifies a polymorphic interface
)
Just stumbled across this, realise its a bit old now but thought I'd share my solution as I had the same requirement. This is only a partial solution but it works me
I have extended the PaperTrail Version class and overriden the item_type getter
class AuditTrail < PaperTrail::Version
xss_foliate :except => [:object, :object_changes]
def item_type
if(self[:item_type] == "SomethingIWantToChange")
return "Different String"
end
return self[:item_type]
end
end
I then have set each of my models to use this class like so:
has_paper_trail :class_name => 'AuditTrail'
Then I can query the versions table and objects returned will run through the overridden getter and the item_type will be as I require:
audit_records = AuditTrail.where(someproperty: "something")
So this doesn't actually alter it in the DB when it is written, but is the best way I could find to present it differently to my frontend
Note that it doesn't alter the item_type if you fetch versions without using a query from your extended object i.e:
Someobject.find(1).versions.last
^ this still returns the item_type from the DB
You want the class_name option. This should work with your example:
class Post < ActiveRecord::Base
has_paper_trail :class_name => 'Foo'
end
This will version the Post class as Foo.
You can find more details in the Paper Trail documentation in the Custom Version Classes section.

Massaging a mongoid habtm with a string for a class

I started off with https://gist.github.com/scttnlsn/1295485 as a basis to make a restful sinatra app. I'm having difficulty, though, managing HaBTM relationships for paths such as
delete '/:objecttype/:objid/:habtm_type/:habtm_id'
I already have the objecttype thanks to the map (as per that gist), and pulling the right object from the db with the id is straightfoward. However, getting the other side of the habtm and calling the appropriate method on objecttype to delete the relationship involves turning a handful of strings into the appropriate objects and methods.
I came up with a solution, but it uses eval. I'm aware that using eval is evil and doing so will rot my very soul. Is there a better way to handle this, or should I put in some safeguards to protect the code and call it a day?
Here's a working, self contained, sinatra-free example to show how I'm doing the eval:
require 'mongoid'
require 'pp'
def go
seed
frank = Person.find_by(name:"Frank")
apt = Appointment.find_by(name:"Arbor day")
pp frank
really_a_sinatra_route(frank.id, "appointments", apt.id)
frank.reload
pp frank
end
def really_a_sinatra_route(id, rel_type,rel_id)
# I use "model" in the actual app, but hardwired a person here to
# make a simpler example
person = Person.find_by(id: id)
person.deassociate(rel_type,rel_id)
end
class Base
def deassociate(relationship,did)
objname = associations[relationship].class_name
# Here's the real question... this scares me as dangerous. Is there
# a safer way to do this?
obj = eval "#{objname}.find(did)"
eval "#{relationship}.delete(obj)"
end
end
class Person < Base
include Mongoid::Document
has_and_belongs_to_many :appointments
end
class Appointment < Base
include Mongoid::Document
has_and_belongs_to_many :persons
end
def seed
Mongoid.configure do |config|
config.connect_to("test_habtmexample")
end
Mongoid.purge!
frank=Person.create(name:"Frank")
joe=Person.create(name:"Joe")
ccon = Appointment.create(name:"Comicon")
aday = Appointment.create(name:"Arbor day")
frank.appointments << ccon
frank.appointments << aday
ccon.persons << joe
joe.reload
end
go
A nice gentleman on freenode helped me out. Those two evals can be replaced with:
obj= self.send(relationship.to_sym).find(did)
self.send(relationship.to_sym).delete(obj)

I18n::Backend::ActiveRecord with scope

I want to enable users to overwrite custom translations in the locales/YAML-files.
I use the i18n-active_record gem by Sven Fuchs which works great to use translations stored in the database.
The problem: Users should only get their own translations, not those of others.
So I added a user_id column to the translations table. Now I have no idea how to setup a scope for I18n::Backend::ActiveRecord.
My locale.rb (in config/initializers):
require 'i18n/backend/active_record'
I18n.backend = I18n::Backend::ActiveRecord.new
I18n::Backend::ActiveRecord.send(:include, I18n::Backend::Memoize)
I18n::Backend::ActiveRecord.send(:include, I18n::Backend::Flatten)
I18n::Backend::Simple.send(:include, I18n::Backend::Memoize)
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
I18n.backend = I18n::Backend::Chain.new(I18n.backend, I18n::Backend::Simple.new)
Thanks for any ideas!
try adding this to an initializer file
ie: added to where you initialize the activerecord backend for i18n
config/initializers/i18n_backend.rb
require 'i18n/backend/active_record'
if ActiveRecord::Base.connection.table_exists? 'translations'
require 'i18n/backend/active_record'
I18n.backend = I18n::Backend::Chain.
new(I18n::Backend::ActiveRecord.new, I18n.backend)
end
# OVERRIDING DEFAULT QUERY
module I18n
module Backend
class ActiveRecord
class Translation < ::ActiveRecord::Base
class << self
def locale(locale)
where(:locale => locale.to_s).where(:field => condition)
end
end
end
end
end
end
this should overrides the default locale method in the i18n-active_record gem

Issue loading classes order EDIT: works, although some odd behavior along the way

I'm working on a project to recreate some of the functionality of ActiveRecord. Here's the portion that isn't working
module Associations
def belongs_to(name, params)
self.class.send(:define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
self.class.send(:define_method, :other_table_name) do |other_class|
other_class.table_name
end
.
.
.
o_c = other_class(name, params)
#puts this and other (working) values in a query
query = <<-SQL
...
SQL
#sends it off with db.execute(query)...
I'm building towards this testing file:
require 'all_files' #holds SQLClass & others
pets_db_file_name = File.expand_path(File.join(File.dirname(__FILE__), "pets.db"))
DBConnection.open(pets_db_file_name)
#class Person
#end
class Pet < SQLClass
set_table_name("pets")
set_attrs(:id, :name, :owner_id)
belongs_to :person, :class_name => "Person", :primary_key => :id, :foreign_key => :owner_id
end
class Person < SQLClass
set_table_name("people")
set_attrs(:id, :name)
has_many :pets, :foreign_key => :owner_id
end
.
.
.
Without any changes I received
.../active_support/inflector/methods.rb:230:in `block in constantize': uninitialized constant Person (NameError)
Just to make sure that it was an issue with the order of loading the classes in the file I began the file with the empty Person class, which, as predicted gave me
undefined method `table_name' for Person:Class (NoMethodError)
Since this is a learning project I don't want to change the test to make my code work (open all the classes, set all the tables/attributes then reopen them them for belongs_to. But, I'm stuck on how else to proceed.)
EDIT SQLClass:
class SQLClass < AssignmentClass
extend SearchMod
extend Associations
def self.set_table_name(table_name)
#table_name = table_name
end
def self.table_name
#table_name
end
#some more methods for finding rows, and creating new rows in existing tables
And the relevant part of AssignmentClass uses send on attr_accessor to give functionality to set_attrs and makes sure that before you initialize a new instance of a class all the names match what was set using set_attrs.
This highlights an important difference between dynamic, interpreted Ruby (et al) and static, compiled languages like Java/C#/C++. In Java, the compiler runs over all your source files, finds all the class/method definitions, and matches them up with usages. Ruby doesn't work like this -- a class "comes into existence" after executing its class block. Before that, the Ruby interpreter doesn't know anything about it.
In your test file, you define Pet first. Within the definition of Pet, you have belongs_to :person. belongs_to does :person.constantize, attempting to get the class object for Person. But Person doesn't exist yet! Its definition comes later in the test file.
There are a couple ways I can think that you could try to resolve this:
One would be to do what Rails does: define each class in its own file, and make the file names conform to some convention. Override constant_missing, and make it automatically load the file which defines the missing class. This will make load order problems resolve themselves automatically.
Another solution would be to make belongs_to lazy. Rather than looking up the Person class object immediately, it could just record the fact that there is an association between Pet and Person. When someone tries to call pet.person, use a missing_method hook to actually define the method. (Presumably, by that time all the class definitions will have been executed.)
Another way would be do something like:
define_method(belongs_to) do
belongs_to_class = belongs_to.constantize
self.class.send(:define_method, belongs_to) do
# put actual definition here
end
self.send(belongs_to)
end
This code is not tested, it's just to give you an idea! Though it's a pretty mind-bending idea, perhaps. Basically, you define a method which redefines itself the first time it is called. Just like using method_missing, this allows you to delay the class lookup until the first time the method is actually used.
If I can say one more thing: though you say you don't want to "overload" method_missing, I don't think that's as much of a problem as you think. It's just a matter of extracting code into helper methods to keep the definition of method_missing manageable. Maybe something like:
def method_missing(name,*a,&b)
if has_belongs_to_association?(name)
invoke_belongs_to_association(name,a,b)
elsif has_has_many_association?(name)
invoke_has_many_association(name,a,b)
# more...
else
super
end
end
Progress! Inspired by Alex D's suggestion to use method_missing to delay the creation I instead used define_methodto create a method for the name, like so:
define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
define_method(:other_table_name) do |other_class|
other_class.table_name
end
#etc
define_method(name) do #|params| turns out I didn't need to pass in `params` at all but:
#p "---#{params} (This is line 31: when testing this out I got the strangest error
#.rb:31:in `block in belongs_to': wrong number of arguments (0 for 1) (ArgumentError)
#if anyone can explain this I would be grateful.
#I had declared an #params class instance variable and a getter for it,
#but nothing that should make params require an argument
f_k = foreign_key(name, params)
p f_k
o_c = other_class(name, params)
o_t_n = other_table_name(o_c)
p_k = primary_key(params)
query = <<-SQL
SELECT *
FROM #{o_t_n}
WHERE #{p_k} = ?
SQL
row = DBConnection.execute(query, self.send(f_k))
o_c.parse_all(row)
end

does a sequel models generator exists?

I'm looking for a ruby class that could generate the sequel model file for Ramaze after reading the definition of the table in a mySQL database.
For example, I would like to type :
ruby mySuperGenerator.rb "mytable"
And the result shold be the file "mytable.rb" in "model" directory, containing :
class Mytable < Sequel::Model(:mytable)
# All plugins I've defined somewhere before lauching the generator
plugin :validation_helpers
plugin :json_serializer
one_to_many :othertable
many_to_one :othertable2
def validate
# Generating this if there are some not null attributes in this table
validates_presence [:fieldthatshoulnotbenull1, :fieldthatshoulnotbenull2]
errors.add(:fieldthatshoulnotbenull1, 'The field fieldthatshoulnotbenull1 should not be null.') if self.fieldthatshoulnotbenull1.nil?
end
def before_create
# All the default values found for each table attributes
self.creation_time ||= Time.now
end
def before_destroy
# referential integrity
self.othertable_dataset.destroy unless self.othertable.nil?
end
end
Does someone knows if such a generator exists ?
Well...
I finally wrote my script.
see https://github.com/Pilooz/sequel_model_generator Have look and fork !

Resources