I have two User objects existing_user, current_user, how should I traverse the User.attributes.keys and check if they are equal.
Probably something like this:
class User < ActiveRecord::Base
# ...
# untested, but the logic seems sound.
def equals?(user)
User.attributes.keys.each do |k|
return false unless self[k] == user[k]
end
true
end
end
You could then call current_user.equals?(existing_user).
The background of this question is not completely clear to me. Usually when one is refering to current_user like you do, the background is ruby-on-rails, not only ruby like stated in the subject. I guess, you are getting the current_user via an external login-process (cas, facebook, etc) and internally keep a user-table for additional attributes.
Given my assumption is true, I would recommend, to identify one uniq attribute of the external user-model and store it in your internal user-table. You might want to validate this attributes uniqueness. When making your comparison, restrict to comparing this single attribute on the externally logged-in user and the internally kept users.
Related
In our Rails application, the Post resource can be made by either a User or an Admin.
Thus, we have an ActiveRecord model class called Post, with a belongs_to :author, polymorphic: true.
However, in certain conditions, the system itself is supposed to be able to create posts.
Therefore, I'm looking for a way to add e.g. System as author.
Obviously, there will only ever be one System, so it is not stored in the database.
Naïvely attempting to just add an instance (e.g. the singleton instance) of class System; end as author returns errors like NoMethodError: undefined method `primary_key' for System:Class.
What would be the cleanest way to solve this?
Is there a way to write a 'fake' ActiveRecord model that is not actually part of the database?
There's two ways that I see that make the most sense:
Option A: Add a 'system' Author record to the DB
This isn't a horrible idea, it just shifts the burden onto you making sure certain records are present in every environment. But you can always create these records in seed files if you want to ensure they're always created.
The benefit over option B is that you can just use standard ActiveRecord queries to find all of the system's Posts.
Option B: Leave the association nil and add a new flag for :created_by_system
This is what I would opt for. If a Post was made by the system, just leave the author reference blank and set a special flag to indicate this model was created internally.
You can still have a method to quickly get a list of all of them just by making a scope:
scope :from_system, -> { where(created_by_system: :true) }
Which one you choose I think depends on whether you want to be able to query Post.author and get information about the System. In that case you need to take option A. Otherwise, I would use option B. I'm sure there's some other ways to do it too but I think this makes the most sense.
Finally I ended up with creating the following 'fake' model class that does not require any changes to the database schema.
It which leverages a bit of meta-programming:
# For the cases in which the System itself needs to be given an identity.
# (such as when it does an action normally performed by a User or Admin, etc.)
class System
include ActiveModel::Model
class << self
# The most beautiful kind of meta-singleton
def class
self
end
def instance
self
end
# Calling`System.new` is a programmer mistake;
# they should use plain `System` instead.
private :new
def primary_key
:id
end
def id
1
end
def readonly?
true
end
def persisted?
true
end
def _read_attribute(attr)
return self.id if attr == :id
nil
end
def polymorphic_name
self.name
end
def destroyed?
false
end
def new_record?
false
end
end
end
Of main note here is that System is both its own class and its own instance.
This has the following advantages:
We can just pass Post.new(creator: System) rather than System.new or System.instance
There is at any point only one system.
We can define the class methods that ActiveRecord requires (polymorphic_name) on System itself rather than on Class.
Of course, whether you like this kind of metaprogramming or find it too convoluted is very subjective.
What is less subjective is that overriding ActiveRecord's _read_attribute is not nice; we are depending on an implementation detail of ActiveRecord. Unfortunately to my knowledge there is no public API exposed that could be used to do this more cleanly. (In our project, we have some specs in place to notify us immediately when ActiveRecord might change this.)
I'm writing a web app using Rails, part of which includes giving users the ability to leave reviews for things. I wanted to put a validation in the review model to ensure that one user can't leave multiple reviews of the same item, so I wrote this:
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where({user_id: record.user,
work_id: record.work})
unless dup_reviews.length < 1
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
This validator has the desired behavior, i.e. it guarantees that a user can't review a work twice. However, it has the undesired side-effect that a user can't update an already existing review that he/she left. I'm using a really simple
def update
#review.update(review_params)
respond_with(#work)
end
in the reviews controller. How can I change either the validator or the update method so that duplicate reviews are prevented but updates are allowed?
I'm very new to Rails and web development, so I'm sure I've done something goofy here. I didn't use one of the built-in unique validators because what is unique is the user/work pair; there can more than one review by the same user of different works, and there can be more than one review of the same work by different users.
You can use validates_uniqueness_of on multiple attributes, like this:
validates_uniqueness_of :user_id, :scope => :work_id
Then a user would not be allowed to review a already reviewed work.
#Sharvy Ahmed's answer is definitely the best, as long as the case is simple enough – the OP's case seems like one of them.
However, if the conditions are more complex, you may need/want to write your custom validation. For that purpose, here's an example (checked with Rails 6.0).
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where(user_id: record.user,
work_id: record.work)
dup_reviews = dup_reviews.where.not(id: record.id) unless record.new_record?
if dup_reviews.count > 0
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
The idea is,
In create, all the relevant DB records retrieved with where can and should be used to judge the uniqueness. In the example new_record? is used to check it out, but it is actually redundant (because nil id matches no records).
In update, the DB row of the record to update must be excluded from the unique comparison. Otherwise, the update would always fail in the validation.
The count method is slightly more efficient in terms of DB transaction.
I've been reading up on rails security concerns and the one that makes me the most concerned is mass assignment. My application is making use of attr_accessible, however I'm not sure if I quite know what the best way to handle the exposed relationships is. Let's assume that we have a basic content creation/ownership website. A user can have create blog posts, and have one category associated with that blog post.
So I have three models:
user
post: belongs to a user and a category
category: belongs to user
I allow mass-assignment on the category_id, so the user could nil it out, change it to one of their categories, or through mass-assignment, I suppose they could change it to someone else's category. That is where I'm kind of unsure about what the best way to proceed would be.
The resources I have investigated (particularly railscast #178 and a resource that was provided from that railscast), both mention that the association should not be mass-assignable, which makes sense. I'm just not sure how else to allow the user to change what the category of the post would be in a railsy way.
Any ideas on how best to solve this? Am I looking at it the wrong way?
UPDATE: Hopefully clarifying my concern a bit more.
Let's say I'm in Post, do I need something like the following:
def create
#post = Post.new(params[:category])
#post.user_id = current_user.id
# CHECK HERE IF REQUESTED CATEGORY_ID IS OWNED BY USER
# continue on as normal here
end
That seems like a lot of work? I would need to check that on every controller in both the update and create action. Keep in mind that there is more than just one belongs_to relationship.
Your user can change it through an edit form of some kind, i presume.
Based on that, Mass Assignment is really for nefarious types who seek to mess with your app through things like curl. I call them curl kiddies.
All that to say, if you use attr_protected - (here you put the fields you Do Not want them to change) or the kid's favourite attr_accessible(the fields that are OK to change).
You'll hear arguments for both, but if you use attr_protected :user_id in your model, and then in your CategoryController#create action you can do something like
def create
#category = Category.new(params[:category])
#category.user_id = current_user.id
respond_to do |format|
....#continue on as normal here
end
OK, so searched around a bit, and finally came up with something workable for me. I like keeping logic out of the controllers where possible, so this solution is a model-based solution:
# Post.rb
validates_each :asset_category_id do |record, attr, value|
self.validates_associated_permission(record, attr, value)
end
# This can obviously be put in a base class/utility class of some sort.
def self.validates_associated_permission(record, attr, value)
return if value.blank?
class_string = attr.to_s.gsub(/_id$/, '')
klass = class_string.camelize.constantize
# Check here that the associated record is the users
# I'm leaving this part as pseudo code as everyone's auth code is
# unique.
if klass.find_by_id(value).can_write(current_user)
record.errors.add attr, 'cannot be found.'
end
end
I also found that rails 3.0 will have a better way to specify this instead of the 3 lines required for the ultra generic validates_each.
http://ryandaigle.com/articles/2009/8/11/what-s-new-in-edge-rails-independent-model-validators
In one of my model objects I have an array of objects.
In the view I created a simple form to add additional objects to the array via a selection box.
In the controller I use the append method to add user selected objects to the array:
def add_adjacents
#site = Site.find(params[:id])
if request.post?
#site.adjacents << Site.find(params[:adjacents])
redirect_to :back
end
end
I added a validation to the model to validate_the uniqueness_of :neighbors but using the append method appears to be bypassing the validation.
Is there a way to force the validation? Or a more appropriate way to add an element to the array so that the validation occurs? Been googling all over for this and going over the books, but can't find anything on this.
Have you tried checking the validity afterwards by calling the ".valid?" method, as shown below?
def add_adjacents
#site = Site.find(params[:id])
#site.neighbors << Site.find(params[:neighbors])
unless #site.valid?
#it's not valid, do something to fix it!
end
end
A couple of comments:
Then only way to guarantee uniqueness is to add a unique constraint on your database. validates_uniqueness_of has it's gotchas when there are many users in the system:
Process 1 checks uniqueness, returns true.
Process 2 checks uniqueness, returns true.
Process 1 saves.
Process 2 saves.
You're in trouble.
Why do you have to test for request.post?? This should be handled by your routes, so in my view it's logic that is fattening your controller unnecessarily. I'd imagine something like the following in config/routes.rb: map.resources :sites, :member => { :add_adjacents => :post }
Need to know more about your associations to figure out how validates_uniqueness_of should play in with this setup...
I think you're looking for this:
#site.adjacents.build params[:adjacents]
the build method will accept an array of attribute hashes. These will be validated along with the parent model at save time.
Since you're validating_uniqueness_of, you might get some weirdness when you are saving multiple conflicting records at the same time, depending on the rails implementation for the save and validation phases of the association.
A hacky workaround would be to unique your params when they come in the door, like so:
#site.adjacents.build params[:adjacents].inject([]) do |okay_group, candidate|
if okay_group.all? { |item| item[:neighbor_id] != candidate[:neighbor_id] }
okay_group << candidate
end
okay_group
end
For extra credit you can factor this operation back into the model.
I need to cache (and expire) all the models in a table.
For example, if i have a model named Currency, i only have less than 10 possible currencies. Therefore, it would be nice to have:
class Currency < ActiveRecord::Base
cache_all(:expire_in => 10.minutes)
end
so that
Currency.all
Currency.find_by_name("USD")
should not hit the DB.
What do you think it could be a good approach?
Also, if you believe it would be better to use a model that is not backed up by a DB, please comment on this. Please notice that i would like to have a AR-style association.
Since the data set is so small, probably the best thing is to cache it in local memory. There are a couple ways to do this, one is to use Memoization like I show here. However that isn't the most efficient because it will store the all method and find_by_name method in separate caches even though they are the same object.
An alternative is to redefine the methods to cache the objects manually. Something like this.
class Currency < ActiveRecord::Base
def self.all
#all_cache ||= super.map(&:freeze) # freeze so you don't modify the cached objects
end
def self.find_by_name(name)
all.detect { |c| c.name.to_s.downcase == name.to_s.downcase }
end
def self.flush_all_cache
#all_cache = nil
end
end
There may be a plugin to handle this for you, but I haven't looked into that at all.