I've not seen this feature as a plug in and was wondering how to achieve it, hopefully using rails.
The feature I'm after is the ability to rate one object (a film) by various attributes such as plot, entertainment, originality etc etc on one page/form.
Can anyone help?
I don't think you need a plugin to do just that... you could do the following with AR
class Film < ActiveRecord::Base
has_many :ratings, :as => :rateable
def rating_for(field)
ratings.find_by_name(field).value
end
def rating_for=(field, value)
rating = nil
begin
rating = ratigns.find_by_name(field)
if rating.nil?
rating = Rating.create!(:rateable => self, :name => field, :value => value)
else
rating.update_attributes!(:value => value)
end
rescue ActiveRecord::RecordInvalid
self.errors.add_to_base(rating.errors.full_messages.join("\n"))
end
end
end
class Rating < ActiveRecord::Base
# Has the following field:
# column :rateable_id, Integer
# column :name, String
# column :value, Integer
belongs_to :rateable, :polymorphic => true
validates_inclusion_of :value, :in => (1..5)
validates_uniqueness_of :name, :scope => :rateable_id
end
Of course, with this approach you would have a replication in the Rating name, something that is not that bad (normalize tables just for one field doesn't cut it).
You can also use a plugin ajaxfull-rating
Here's another, possibly more robust rating plugin...it's been around for a while and has been revised to work with Rails 2
http://agilewebdevelopment.com/plugins/acts_as_rateable
Related
I couldn't easily find a relevant topic on SO so here we go - a classic problem: I have a User or Person model and I want to model that person's physical properties/attributes (eye color, hair color, skin color, gender, some lifestyle properties like sleep time per night (<8h, ~8h, >8h), smoking, daily sun exposure etc.
I usually solve that problem by creating a separate rails model (database table) for each property because then it is easy to add more options later, edit them, use as a source for <select>, i.e.
class Person
belongs_to :eye_color
belongs_to :skin_color
belongs_to :hair_color
belongs_to :sleep_per_night
belongs_to :sun_exposure
attr_accessible :gender # boolean, m/f
end
class HairColor
has_many :people
attr_accessible :value
end
class EyeColor
has_many :people
attr_accessible :value
end
Person.last.eye_color
...
EyeColor.first.people
But what if there is a lot of those attributes (i.e. 10-15 different phisycal and lifestyle properties). For me it seems like breaking DRY rule, that is, I'm left with many small tables, like eye_colors, which have 3-5 records. Each of those tables has only one meaningful column - value.
I was thinking how you guys solve those problems, maybe by creating a single model, i.e. PersonProperty that has the following structure
person_properties[type, value]
so the previous solution with separate models, i.e. for eye_color and hair_color would look like this (types/classes and values):
# PersonProperty/person_properties:
1. type: 'HairColor', value: 'red'
2. type: 'HairColor', value: 'blond'
3. type: 'SkinColor', value: 'white'
4. type: 'EyeColor', value: 'green'
5. type: 'HairColor', value: 'black'
6. type: 'SkinColor', value: 'yellow'
7. type: 'SleepPerNight', value: 'less than 8h'
8. type: 'SleepPerNight', value: 'more than 8h'
9. type: 'DailySunExposure', value: 'more than 1h'
...
19. type: 'EyeColor', value: 'blue'
...
The above example might be more normalized by splitting the PersonProperty model into two. Or maybe you suggest something else?
I wouldn't suggest a has_one relationship in this case. A User could belongs_to :eye_color, so you can map eyecolors across your users. An EyeColor has_many :users, so that you can do #eye_color.users and get all users with a specific EyeColor. Otherwise you will have to create an EyeColor for every user (or at least the ones with eyes).
The reason I'd suggest this over your PersonProperty solution is because it's easier to maintain and because of the performance gain of delegating these kinds of relations to your database.
UPDATE: If dynamic attributes are what you want, I'd suggest to setup your models like this:
class Person < ActiveRecord::Base
has_many :person_attributes
attr_accessible :gender
accepts_nested_attributes_for :person_attributes
end
class PersonAttribute < ActiveRecord::Base
belongs_to :person_attribute_type
belongs_to :person_attribute_value
belongs_to :person
attr_accessible :person_id, :person_attribute_value_id
end
class PersonAttributeValue < ActiveRecord::Base
has_many :person_attributes
belongs_to :person_attribute_type
attr_accessible :value, :person_attribute_type_id
end
class PersonAttributeType < ActiveRecord::Base
has_many :person_attribute_values
attr_accessible :name, :type
end
This way you can do the following:
#person_attribute_type = PersonAttributeType.create(:name => 'Eye color', :type => 'string')
['green', 'blue', 'brown'].each do |color|
#person_attribute_type.person_attribute.values.build(:value => color)
end
#person_attribute_type.save
#person = Person.new
#person_attribute = #person.person_attributes.build
#person_attribute.person_attribute_value = #person_attribute_type.person_attribute_values.find(:value => 'green')
Of course, you probably won't fill your database through the command line. You will probably be very curious as to how this would work in a form:
class PersonController
# ...
def new
#person = Person.new
PersonAttributeType.all.each do |type|
#person.person_attributes.build(:person_attribute_type = type)
end
end
def create
#person = Person.new(params[:person])
if #person.save
# ...
else
# ...
end
end
def edit
#person = Person.find(params[:id])
PersonAttributeType.where('id NOT IN (?)', #person.person_attributes.map(&:person_attribute_type_id)).each do |type|
#person.person_attributes.build(:person_attribute_type = type)
end
end
# ...
Now the form, based on Formtastic:
semantic_form_for #person do |f|
f.input :gender, :as => :select, :collection => ['Male', 'Female']
f.semantic_fields_for :person_attributes do |paf|
f.input :person_attribute_value, :as => :select, :collection => paf.object.person_attribute_type.person_attributes_values, :label => paf.object.person_attribute_type.name
end
f.buttons
end
Mind that this all is untested, so just try to understand what I'm trying to do here.
BTW, I now realize that the class name PersonAttribute is probably a bit unlucky, because you will have to accepts_nested_attributes_for :person_attributes which would mean you would have to attr_accessible :person_attributes_attributes, but you get my drift I hope.
Here are the model relationships:
class Tool < ActiveRecord::Base
...
has_many :loss_ratios, :dependent => :destroy, :order => "loss_ratios.starts_at DESC"
validates_associated :loss_ratios
accepts_nested_attributes_for :loss_ratios, :allow_destroy => true
attr_accessible :name, :text, :service_id, :supplier_id, :loss_ratios_attributes
end
class LossRatio < ActiveRecord::Base
belongs_to :tool
validates :rate, :starts_at, :tool, :presence => true
validates_uniqueness_of :starts_at, :scope => :tool_id
validates_numericality_of :rate
validates_inclusion_of :rate, :in => (0..1)
...
end
I'm managing LossRatio associations in the create/update ToolsController actions. I want to test those by POSTing sets of attributes for a Tool (including a couple of nested LossRatios as though they were submitted in a form). I'm using FactoryGirl, but it doesn't seem to have a way of constructing a params-like hash of attributes (attributes_for ignores associations, and looks like this behavior is not gonna change).
Is there a way to do this?
(I know the title is a mess, but I couldn't think of anything better and shorter...)
OK here's what I've come up with after pulling my hair out for half a day:
def params_for(factory_name)
exclude_params = [ "id", "created_at", "updated_at" ]
f = FactoryGirl.build(factory_name)
params = f.attributes.except(*exclude_params).dup
f.reflections.select { |k,v|
v.macro == :has_many && !v.instance_of?(ActiveRecord::Reflection::ThroughReflection)
}.each_key do |k|
assoc_collection = f.send(k)
unless assoc_collection.empty?
params["#{k.to_s}_attributes"] = {}
assoc_collection.each_with_index do |assoc_obj,idx|
params["#{k.to_s}_attributes"][idx.to_s] = assoc_obj.attributes.except(*exclude_params << "#{f.class.name.underscore}_id")
end
end
end
params
end
This is a helper method used for building a params hash consumable by a controller's CRUD actions. I'm using it in my controller specs like so:
subject { post :create, :tool => params_for(:tool_with_lr_history) }
it "creates a new tool" do
expect { subject }.to change(Tool, :count).by(1)
end
As seen from the snippet, the method only populates attributes for has-many associations (and ignores has-many-through associations). I guess it might be extended for any kind of relationships, but that works for me so far (unless there's a better way of doing what I want)...
I have a Project model which accepts nested attributes for Task.
class Project < ActiveRecord::Base
has_many :tasks
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
validates_uniqueness_of :name
end
Uniqueness validation in Task model gives problem while updating Project.
In edit of project i delete a task T1 and then add a new task with same name T1, uniqueness validation restricts the saving of Project.
params hash look something like
task_attributes => { {"id" =>
"1","name" => "T1", "_destroy" =>
"1"},{"name" => "T1"}}
Validation on task is done before destroying the old task. Hence validation fails.Any idea how to validate such that it doesn't consider task to be destroyed?
Andrew France created a patch in this thread, where the validation is done in memory.
class Author
has_many :books
# Could easily be made a validation-style class method of course
validate :validate_unique_books
def validate_unique_books
validate_uniqueness_of_in_memory(
books, [:title, :isbn], 'Duplicate book.')
end
end
module ActiveRecord
class Base
# Validate that the the objects in +collection+ are unique
# when compared against all their non-blank +attrs+. If not
# add +message+ to the base errors.
def validate_uniqueness_of_in_memory(collection, attrs, message)
hashes = collection.inject({}) do |hash, record|
key = attrs.map {|a| record.send(a).to_s }.join
if key.blank? || record.marked_for_destruction?
key = record.object_id
end
hash[key] = record unless hash[key]
hash
end
if collection.length > hashes.length
self.errors.add_to_base(message)
end
end
end
end
As I understand it, Reiner's approach about validating in memory would not be practical in my case, as I have a lot of "books", 500K and growing. That would be a big hit if you want to bring all into memory.
The solution I came up with is to:
Place the uniqueness condition in the database (which I've found is always a good idea, as in my experience Rails does not always do a good job here) by adding the following to your migration file in db/migrate/:
add_index :tasks [ :project_id, :name ], :unique => true
In the controller, place the save or update_attributes inside a transaction, and rescue the Database exception. E.g.,
def update
#project = Project.find(params[:id])
begin
transaction do
if #project.update_attributes(params[:project])
redirect_to(project_path(#project))
else
render(:action => :edit)
end
end
rescue
... we have an exception; make sure is a DB uniqueness violation
... go down params[:project] to see which item is the problem
... and add error to base
render( :action => :edit )
end
end
end
For Rails 4.0.1, this issue is marked as being fixed by this pull request, https://github.com/rails/rails/pull/10417
If you have a table with a unique field index, and you mark a record
for destruction, and you build a new record with the same value as the
unique field, then when you call save, a database level unique index
error will be thrown.
Personally this still doesn't work for me, so I don't think it's completely fixed yet.
Rainer Blessing's answer is good.
But it's better when we can mark which tasks are duplicated.
class Project < ActiveRecord::Base
has_many :tasks, inverse_of: :project
accepts_nested_attributes_for :tasks, :allow_destroy => :true
end
class Task < ActiveRecord::Base
belongs_to :project
validates_each :name do |record, attr, value|
record.errors.add attr, :taken if record.project.tasks.map(&:name).count(value) > 1
end
end
Ref this
Why don't you use :scope
class Task < ActiveRecord::Base
validates_uniqueness_of :name, :scope=>'project_id'
end
this will create unique Task for each project.
I have 2 models A and B.
class A < ActiveRecord::Base
has_one :b
acts_as_ferret :fields => [:title,:description]
In a_cotroller, i wrote:
#search=A.find_with_ferret(params[:st][:text_search],:limit => :all).paginate :per_page =>10, :page=>params[:page]
The above title and description search is properly working.
class B < ActiveRecord::Base
belongs_to :a
Now,I want to perform a text search by using 3 fields; title, description(part of A) and comment(part of B). Where I want to include the comment field to perform the ferret search.Then,what other changes needed.
The documentation of find_with_ferret indicates that you simply code :store_class_name => :true to enable search over multiple models. While this is true there is a little more to it. To search multiple do the following:
#search = A.find_with_ferret(
params[:st][:text_search],
:limit => :all,
:multi => [B]
).paginate :per_page =>10, :page=>params[:page]
Notice the multi option. This is an array of the additional indexes to search.
Now to get his to work you have to rebuild your indexes after adding :store_class_name => :true to the index definitions.
class A < ActiveRecord::Base
has_one :b
acts_as_ferret :store_class_name => :true, :fields => [:title, :description]
end
OR...
You can simply include Bs fields in the index definition:
class A < ActiveRecord::Base
has_one :b
acts_as_ferret :fields => [:title, :description],
:additional_fields => [:b_content, :b_title]
def b_content
b.content
end
def b_title
b.title
end
end
This makes everything simple, but doesn't allow to search the B model independently of A.
I'm building an application where I have products and categories. Category has_many properties and each property has a list of possible values. After a category is set to the product all properties show up in the form and the user can set that property to one of the properties possible values.
My question is:
Is it possible for Thinking Sphinx to filter the products through a property and property value ex:
:with => {:property_id => property_value}
If it's possible, what is the best way to implement this? If not is there any other library out there to solve this problem?
Thanks
/ Ola
One approach is to store the property_id as multi-value attribute.
class Product < ActivRecord::Base
has_one :category
has_many :properties, :through => :category
KVP = "###"
define_index do
has properties("CONCAT(`properties`.`key`, \"%s\", `properties`.`value`)" %
KVP, :as => :category_key_value
end
def search_with_properties keys, with_attr={}, p={}
wp = (with_attr||{}).dup
values = p.map{|k, v| "#{k}#{KVP}#{v}"} unless p.empty?
wp = wp.merge({:category_key_value => values}) unless values.empty?
search keys, :with => wp
end
end
class Category < ActivRecord::Base
belongs_to :product
has_many :properties
end
class Property < ActivRecord::Base
belongs_to :Category
#key E.g: region
#value E.g: South West
end
Now you can issue following search commands:
Product.search_with_properties("XYZ", nil, :region => "South West")
Try this:
Add the following to your define_index:
has properties(:id), :as => :property_ids
Then you can use :with / :without like:
:with => {:property_ids => property_value}
Does this answer your question:
https://github.com/freelancing-god/thinking-sphinx/issues/356