Rails3: Cascading Select Writer's Block - ruby-on-rails-3

I have a big, flat table:
id
product_id
attribute1
attribute2
attribute3
attribute4
Here is how I want users to get to products:
See a list of unique values for attribute1.
Clicking one of those gets you a list of unique values for attribute2.
Clicking one of those gets you a list of unique values for attribute3.
Clicking one of those gets you a list of unique values for attribute4.
Clicking one of those shows you the relevant products.
I have been coding Rails for about 4 years now. I just can't unthink my current approach to this problem.
I have major writer's block. Seems like such an easy problem. But I either code it with 4 different "step" methods in my controller, or I try to write one "search" method that attempts to divine the last level you selected, and all the previous values that you selected.
Both are major YUCK and I keep deleting my work.
What is the most elegant way to do this?

Here is a solution that may be an option. Just off the top of my head and not tested (so there is probably a bit more elegant solution). You could use chained scopes in your model:
class Product < ActiveRecord::Base
scope :with_capacity, lambda { |*args| args.first.nil? ? nil : where(:capacity=>args.first) }
scope :with_weight, lambda { |*args| args.first.nil? ? nil : where(:weight=>args.first) }
scope :with_color, lambda { |*args| args.first.nil? ? nil : where(:color=>args.first) }
scope :with_manufacturer, lambda { |*args| args.first.nil? ? nil : where(:manufacturer=>args.first) }
self.available_attributes(products,attribute)
products.collect{|product| product.send(attribute)}.uniq
end
end
The code above will give you a scope for each attribute. If you pass a parameter to the scope, then it will give you the products with that attribute value. If the argument is nil, then the scope will return the full set (I think ;-). You could keep track of the attributes they are drilling down in in the session with 2 variables (page_attribute and page_attribute_value) in your controller. Then you call the entire chain to get your list of products (if you want to use them on the page). Next you can get the attribute values by passing in the set of products and the attribute name to Product.available_attributes. Note that this method (Product.available_attributes) is a total hack and would be inefficient for a large set of data, so you may want to make this another scope and use :select=>"DISTINCT(your_attribute)" or something more database efficient instead of iterating thru the full set of products as I did in the hack method.
class ProductsController < ApplicationController
def show
session[params[:page_attribute].to_sym] = params[:page_attribute_value]
#products = Product.all.with_capacity(session[:capacity]).with_weight(session[:weight]).with_color(session[:color]).with_manufacturer(session[:manufacturer])
#attr_values = Product.available_attributes(#products,params[:page_attribute])
end
end
Again, I want to warn you that I did not test this code, so its totally possible that some of the syntax is incorrect, but hopefully this will give you a starting point. Holla if you have any questions about my (psuedo) code.

Related

Difference between update and update_attributes

In Rails 5, what is the difference between update and update_attributes methods. I'm seeing the following results for both the methods
Returns true/false
Checking for active record validation
Call backs are triggered
and also regarding update method a new thing was introduced in active record relation. I'm not able to understand it. What is the difference?
Moreover are we using update_attributes in Rails 5. It's not there in active record documentation.
I'm confused with all update methods. I need clarity
As of Rails 4.0.2, #update returns false if the update failed. Before Rails 4.0.2, #update returned the object that got updated. The main difference therefore was the return value. After this change, #update_attributes is just an alias of #update. It seems there are talks to deprecate #update_attributes in Rails 6 which is not released yet.
https://github.com/rails/rails/pull/31998
https://github.com/rails/rails/commit/5645149d3a27054450bd1130ff5715504638a5f5
From the rails 5 files it seems to me update can be used to update multiple objects(array of records) but update_attributes only work on single records otherwise both are same
From rails core files for update_attributes:
Updates a single attribute and saves the record.
This is especially useful for boolean flags on existing records. Also note that
Validation is skipped.
\Callbacks are invoked.
updated_at/updated_on column is updated if that column is available.
Updates all the attributes that are dirty in this object.
This method raises an ActiveRecord::ActiveRecordError if the
attribute is marked as readonly.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
public_send("#{name}=", value)
save(validate: false)
end
For Update
Updates an object (or multiple objects) and saves it to the database, if validations pass.
The resulting object is returned whether the object was saved successfully to the database or not.
==== Parameters
+id+ - This should be the id or an array of ids to be updated.
+attributes+ - This should be a hash of attributes or an array of hashes.
==== Examples
# Updates one record
Person.update(15, user_name: "Samuel", group: "expert")
# Updates multiple records
people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
Person.update(people.keys, people.values)
# Updates multiple records from the result of a relation
people = Person.where(group: "expert")
people.update(group: "masters")
Note: Updating a large number of records will run an UPDATE
query for each record, which may cause a performance issue.
When running callbacks is not needed for each record update,
it is preferred to use {update_all}[rdoc-ref:Relation#update_all]
for updating all records in a single query.
def update(id, attributes)
if id.is_a?(Array)
id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
object.update(attributes[idx])
}
else
if ActiveRecord::Base === id
raise ArgumentError,
"You are passing an instance of ActiveRecord::Base to `update`. " \
"Please pass the id of the object by calling `.id`."
end
object = find(id)
object.update(attributes)
object
end
end
When we are working with update_column that time update is done on the database level there is no any contact with the rails ORM so whatever logic we have implemented like callbacks and validations all will be waste and wont be useful as this is going to be bypassed.
I found this article explained really well in just 30 seconds.
.update
Use update when you want to return false, for example in an if/else:
if record.update(params)
display_success
else
react_to_problem
end
.update!
Use update! when you want an error (for example: to avoid erroring silently, which could be very bad if an error was unexpected and you needed to know about it to fix it!):
record.update!(params) # raises is invalid
'update' respects the validation rules on model, while 'update_attributes' ignores validations.

How do I implement, for instance, "group membership" many-to-many in Parse.com REST Cloud Code?

A user can create groups
A group had to have created by a user
A user can belong to multiple groups
A group can have multiple users
I have something like the following:
Parse.Cloud.afterSave('Group', function(request) {
var creator = request.user;
var group = request.object;
var wasGroupCreated = group.existed;
if(wasGroupCreated) {
var hasCreatedRelation = creator.relation('hasCreated');
hasCreatedRelation.add(group);
var isAMemberOfRelation = creator.relation('isMemberOf');
isAMemberOfRelation.add(group);
creator.save();
}
});
Now when I GET user/me with include=isMemberOf,hasCreated, it returns me the user object but with the following:
hasCreated: {
__type: "Relation"
className: "Group"
},
isMemberOf: {
__type: "Relation"
className: "Group"
}
I'd like to have the group objects included in say, 'hasCreated' and 'isMemberOf' arrays. How do I pull that using the REST API?
More in general though, am I approaching this the right way? Thoughts? Help is much appreciated!
First off, existed is a function that returns true or false (in your case the wasGroupCreated variable is always going to be a reference to the function and will tis always evaluate to true). It probably isn't going to return what you expect anyway if you were using it correctly.
I think what you want is the isNew() function, though I would test if this works in the Parse.Cloud.afterSave() method as I haven't tried it there.
As for the second part of your question, you seem to want to use your Relations like Arrays. If you used an array instead (and the size was small enough), then you could just include the Group objects in the query (add include parameter set to isMemberOf for example in your REST query).
If you do want to stick to Relations, realise that you'll need to read up more in the documentation. In particular you'll need to query the Group object using a where expression that has a $relatedTo pointer for the user. To query in this manner, you will probably need a members property on the Group that is a relation to Users.
Something like this in your REST query might work (replace the objectId with the right User of course):
where={"$relatedTo":{"object":{"__type":"Pointer","className":"_User","objectId":"8TOXdXf3tz"},"key":"members"}}

In a Rails ActiveRecord model, is using after_initialize callbacks a very bad idea?

Let's suppose we have this model
class Account < ActiveRecord::Base
after_initialize :set_name
def set_name
self.name = ‘My Account’
end
end
Now I want run a query that returns only some attributes of the model but not all of them, in particular is not returning the "name" attribute that it is used in after_initialize callback
Account.group(:name).select("count(*), id").first
And then this execution raises the following error because the set_name callback uses an attribute that has not been "loaded" or selected into the records returned by the query.
ActiveModel::MissingAttributeError: missing attribute: name
Fortunately for some particular cases I can execute the same sql query without using the Account model at all to get the desired result
sql = Account.group(:name).select("count(*), id").to_sql
ActiveRecord::Base.connection.execute(sql).first
=> #<Mysql2::Result:0x00000106eddbc0>
But the point is, what if I want to get Account objects instead of a Mysql2::Result one? Should the .select method return "complete" objects with all their attributes (e.g. filling the missing columns with Nil's)? Or is just a very bad idea to use after_initialize callbacks for our ActiveRecord models? Of course we can also add some code in the callback to check if the property exists or not but, in my opinion, this is unnatural or sounds weird working in an OO language.
Most uses of after_initialize can be (and SHOULD be) replaced with defaults on the corresponding database columns. If you're setting the property to a constant value, you may want to look into this as an alternative.
EDIT: if the value isn't constant, a call to has_attribute?(:name) will guard against this error - ActiveModel::MissingAttributeError occurs after deploying and then goes away after a while
No, it is not a bad idea, in fact I use it very often at work. The valid use case for this would be when you want code to run before you try and do anything with the object. Here is a breakdown of some of the filters offered.
# Before you intend to do anything with the object
after_initialize
# Before you intend to save the object
before_save
# After you've saved the object
after_save
# Before you save a new record
before_create
# After you create a new object
after_create

Rails 3: Excluding Results by Default

On my site, moderators can flag spammy comments. When these comments are flagged, they get quarantined so they no longer appear in regular views, though they can still be seen in the administrative control panel. At the moment, I exclude them from regular views like so:
#comments = Comment.where(:flagged => false)
I do this in every controller that has comments in it, of which there are many. I get the feeling that there's a cleaner way to handle this in Rails. Perhaps somewhere in the comments model I can specify that when querying for comments, only retrieve those that aren't flagged. If so, how is that done? And even if that's not possible, is there some other way to dry this code?
u can use a default scope
default_scope where(:flagged => false)
see http://apidock.com/rails/ActiveRecord/Base/default_scope/class
the default scope can be ignored using unscoped. See http://apidock.com/rails/ActiveRecord/Base/unscoped/class
i would prefer using a scope rather a default scope since i dont have to override it when all the records are needed. Depends upon the frequency of fetching all/unflagged records.
Make a scope (named 'clean' for this example):
class Comment < ActiveRecord
scope :clean, where(:flagged => false)
end
Then use:
#comments = Comment.clean
For future-proofing, you may may want to add a class method called default_view which just calls clean and use that instead. As your 'default' needs change, just modify the default_view method.

Rails: Load just one attribute not a whole model

Lets say I have a model, Foo, which is big and has lots of components. For a given Ajax query I'm only interested in one particular attribute, bar, which is a column in the foos table.
Is there a simple way I could load just that attribute, and not bother with retrieving the rest of the record? For instance if all I want to know is the bar for Foo with id#__, how could I retrieve that?
You can return only specific columns by calling the select method with a string containing the attributes you want to return. For your example:
Foo.select('bar').first #<Foo bar: 1>
Keep in mind that these objects will act like normal ActiveRecord objects but return nil for any field you did not select, so take care using this functionality.
You can call select on the class name itself or any Relation, so you can chain together the ActiveRecord calls you usually use like where, etc.
I prefer this
User.where(:id => user_id).pluck(:user_name).first #'tom'
Foo.where(:age => 23).pluck(:user_name) #['tom', 'jerry', ...]
Foo.where(<condition>).select('fieldname')
Example
results = Foo.where('is_active = ?', true).select('bar')
Access the selected fields as:
results.map {|res| res.bar} returns an array of bar's
pluck(*column_names)
doc: http://api.rubyonrails.org/classes/ActiveRecord/Calculations.html#method-i-pluck
e.g. Foo.pluck(:bar)
pick(*column_names) select just one top row's columns, docs
Similar to pluck but fetch only one row