This is an optimization question for an existing application, I've made the code generic to both make it annonmous and also easier to understand, instead of our proprietary models I'm describing a Forum discussion type situation. I've modified all this code for this example and not tested it, so if there are any typos I apologize, I'll try to fix them if they are pointed out to me.
Lets say I have a rails app with four models: Event, User, Forum, and Post.
the important relationships are as follows:
User has many events.
Forum has many posts.
Post has many events.
The front end is a single page javascript app, so all database data needs to be returned in json format.
Context:
when a User clicks on a post, an event is created with the name
'Show' which marks the post as no longer new.
The user needs to be
logged in to see which posts are new clicking on a forum calls the
following endpoint:
There are multiple users so the events able is a many to many relationship between posts and users.
example.com/forum/15/all_posts
heres the relevant code:
Forum Controller:
#forums_controller.rb
def all_posts
current_user = User.find(session[:user_id])
forum = Forum.includes(:posts).where(id: params[:id]).take
forum.posts.each do |post|
post.current_user = current_user
end
render json: forum.to_json(
include: [
{ posts: {
methods: [:is_new]
}}
]
)
end
Posts model:
#post.rb (posts model)
has_many :events
attr_accessor :current_user
def is_new
if current_user #user may not be logged in
!!self.events.where(user_id: current_user.id, name: 'Show').take
else
false
end
end
the model is where the action is at, so we've tried to keep logic out of the controller, but since the session is not available in the model we end up with this crazy work around of adding current_user as an attr_accessor so that methods can return the appropriate data for the user in question.... I don't like this but I've never come up with a better way to do it. We've repeated this pattern elsewhere and I would love to hear alternatives.
Here's my problem:
The call to is_new is used on the front end to determine what posts to hi-light but it's also triggering an n+1 scenario If there are 10 posts, this endpoint would net me a total 12 queries which is no good if my events table is huge. If I moved all the logic to the controller I could probably do this in 2 queries.
in short I have two questions:
MOST IMPORTANT: How can I fix this n+1 situation?
Is there a better way in general? I don't like needing an each loop before calling to_json I don't find this pattern to be elegant or easy to understand. at the same time I don't want to move all the code into the controller. What is the rails way to do this?
If working with scope is an option, I will try something like:
class Post < ApplicationRecord
scope :is_new, -> { where(user_id: current_user.id, name: 'Show') } if current_user.id?
end
If is a better option to send the current_user in your case, you can also do it:
class Post < ApplicationRecord
scope :is_new, ->(current_user) {...}
end
This is just pseudo-code to give an example:
First Answer
When I posted this I forgot you are rendering json from ForumsController.
Post
scope :for_user, -> (user = nil) do
includes(events: :users).where(users: {id: user.id}) if user
end
def is_new_for_user?(user = nil)
return true if user.nil?
self.events.empty?{ |e| e.name == 'Show' }
end
PostController
def index
#posts = Post.for_user(current_user)
end
posts/index.html.erb
...
<% if post.is_new_for_user?(current_user) %>
...
<% end
...
Second Answer
This is still pseudo-code. I didn't test anything.
Forum
scope :for_user, -> (user = nil) do
if user
includes(posts: [events: :users]).where(users: {id: user.id})
else
includes(:posts)
end
end
ForumsController
def all_posts
current_user = User.find(session[:user_id])
forum = Forum.for_user(current_user).where(id: params[:id]).take
render json: forum.to_json(
include: [
{ posts: {
methods: [:is_new_for_user?(current_user)]
}}
]
)
end
Related
I'm new to rails. I have a relatively simple question. I have defined a controller that manages friend requests. In the create action, I check to see if the other user has already sent a friend request to the current user. If so, I skip creating another friend request and simply execute the logic that accepts the friend request that already exists. Here is my code:
class FriendRequestsController < ApplicationController
before_filter :authenticate_user!
def create
current_user_id = current_user.id;
recipient_id = params[:recipient_id].to_i;
# check if the other person has already sent a friend request
unless (existing_request = FriendRequest.find_by(
:sender_id => recipient_id,
:recipient_id => current_user_id)).nil?
accept(existing_request)
return redirect_to current_user
end
request = FriendRequest.new(:sender_id => current_user_id,
:recipient_id => recipient_id)
if request.save
flash[:notice] = "Sent friend request."
else
flash[:errors] = request.errors.full_messages
end
redirect_to users_path
end
Should some of the above logic go into the FriendRequest model, instead? If so, how much of it? Is there a good* way I can move the call to FriendRequest.new and request.save into the model, as well, while still maintaining the necessary degree of control in the controller?
*What I mean by "good" is: standard, ruby-ish, rails-ish, easily recognizable, familiar-to-many, popular, accepted, etc.
Is there anything else about my code that stands out as poor practice?
This right here could be improved a bit:
unless (existing_request = FriendRequest.find_by(
:sender_id => recipient_id,
:recipient_id => current_user_id)).nil?
Instead, you could write a method in the friend request model:
def self.find_matching_request(sender_id, receiver_id)
find_by(sender_id: sender_id, receiver_id: receiver_id)
end
This doesn't really save you many keystrokes for this example but it's the type of thing you'd do if you wanted to move logic out of the controller and into the model. Basically making a method which is a wrapper for database interaction logic. The bulk of database interaction is supposed to be done in the model, though many people end up doing it in the controller.
Then in the controller:
existing_request = find_matching_request(current_user_id, recipient_id)
if existing_request
accept(existing_request)
redirect_to current_user
end
Before you were using unless <some_val>.nil? which looks like an antipattern unless you're treating false and nil differently for some reason. More typical would be if <some_val>
I'm trying to include a few other recent articles when someone views a particular article in my Rails app.
I have the following method in my controller:
def show
#article = Article.find(params[:id])
#recents = Article.where(!#article).order("created_at DESC").limit(4).offset(1)
end
As the expert eye might see, #recents isn't correct. It's my best guess. :)
How do I show some recent articles but not repeat the one they are currently viewing?
You should use a scope in the model, for it has a lot of advanteges. Learn about scopes here. Your case should be something like this:
In the model:
class Article < ActiveRecord::Base
scope :recent, ->(article_id) { where.not(id: article_id).order(created_at: :desc).limit(4) }
end
and in the controller:
def show
#article = Article.find(params[:id])
#recent = Article.recent(#article.id)
end
This way the recent scope will always get the four last articles leaving out the article you pass in as an argument. And scopes are chainable so you could do something like this as well:
def some_action
#user = User.find(params[:user_id])
#user_recent_articles = #user.articles.recent(0)
end
You are getting the user recent articles. I pass a zero because the scope asks for an argument. You could create a different scope if you want to do it the cleanest way.
This, assuming a user has_many articles.
Well, hope it helps!
try with #recents = Article.where.not(id: #article.id).order("created_at DESC").limit(4)
click here - section 2.4 :).
I think there is a way of only making one call instead of 2 the way you have it now.
In my new project, I have a resource Bet which other users can only read if they are the owners of the bet or friends of him. The main problem comes when I want to define abilities for the index action. In an index action, the block does not get executed, so I guess it's not an option.
Let's illustrate it. If I wanted only the owner to be able to index the bets, this would be enough:
can :read, Bet, :user => { :id => user.id }
But I need the acceptable ids to be a range, one defined by all the friends of the user. Something like:
if (bet.user == user) || (bet.user.friends.include? user)
can :read, Bet
end
But this is not correct CanCan syntax.
I guess that a lot of people has had problems with CanCan and nested resources, but I still haven't seen any answer to this.
In your Bet model, create a method:
def is_accessible_by? (user)
owner = self.user
owner == user || owner.friends.include?(user)
end
Now in ability.rb, set your CanCan permission:
can :read, Bet { |bet| bet.is_accessible_by?(user) }
EDIT
As you point out, since index actions don't have an instance of the object, the block above won't get executed.
However, it sounds like what you are trying to do - list the bets owned by the user or their friends - should not be handled using CanCan or permissions. I would create a function in my User model:
def bet_listings
friend_bets = friends.inject([]){ |bets, friend| bets<<friend.bets; bets }
self.bets + friend_bets
end
Then in your index action:
#bets = user.bet_listings
Just now I have found a solution, but I don't like it much. It's about creating a custom action and defining abilities for it. For example...
In the controller:
def index
authorize! :index_bets, #user
end
In ability.rb:
can :index_bets, User do |friend|
user == friend || user.friends.include?(friend)
end
It works, but I don't feel great about using it. Isn't out there anything more elegant?
I still can't figure out how to implement this as i am a newbie with this. some people helpt me and said i had to use audited, so it did. this is my controller:
def show
add_breadcrumb 'Contract Bekijken', :contracten_path
#contracten = Contracten.find(params[:id])
#audits = #contracten.audits.collect { |a| a.created_at }
respond_to do |format|
format.html # show.html.erb
format.json { render json: #contracten }
end
end
Here's a pastie of my whole controller. http://pastie.org/4270702
But i don't know if this is right or how to implement this to my views.
I hope someone really can help because i really need this to work this week.
Thanks.
i have a rails app where i can store contracts in a database, it also has persons and factories tables in the database.
Now i would like to have a last modified table.
I would like when people update/add a new record to the database, that it will show the modifications in the div right on the screenshot.
Thanks :D
What you need is a audit history of edits.
You can either implement that yourself (may make sense if there is custom business logic involved you really want to write yourself) by hooking into the ActiveRecord callbacks for that model.
A sample implementation may look like this:
class Contract < ActiveRecord::Base
after_save :audit
def audit
log = AuditLog.new(:user_id => current_user, :action => :update, ...)
log.save
end
end
This would assume you have a AuditLog Model that contains the appropriate fields you want to log to and in your audit method you write to that.
Or, a simpler way is to use the Audited gem that does that for you (but may come with some limitations).
From the documentation of Audited it seems like you can simply fetch a Contract record and use the audits on that model to then access the audited information.
Sample
def show
#contract = Contract.find(params[:id])
#audits = #contract.audits.collect { |a| a.created_at }
end
Now you have all the timestamps of the audits in the #audits variable and can access them from the view using <% #audits.each do ....
From your question it seems like you just need a list based on the updated_at field.
How about - #contract_list = Contract.all.order( "updated_at DESC").limit(10)
Then you can iterate over the list in the view.
Nice looking page!
I have two models:
Thread (id, title)
ThreadParticipation (id, thread_id, user_id)
I want to define something like:
can :create, ThreadParticipation if the user is a ThreadParticipation
example:
for
thread: (1, 'hello world')
thread_participation: (313, 1, 13) -- where 13 is the user_id
I tried:
can :create, ThreadParticipation, :thread_participations => { :user_id => current_user.id }
But that errors. Any ideas?
Thread in general is just a bad model name, because it will clash with the Thread class defined in Ruby. I implemented this just recently. In my example, I have forums and topics. A person shouldn't be able to create a new topic in a forum they don't have read access to.
I defined a custom permission on the Forum object called create_topic for this:
can :create_topic, Forem::Forum do |forum|
can?(:read, forum) && user.can_create_forem_topics?(forum)
end
Where can_create_forem_topics? is just defined on the User model like this:
def can_create_forem_topics?(forum)
# code goes here
end
Then I just use this custom :create_topic ability in the new and create actions in my TopicsController, where #forum is defined by a before_filter:
authorize! :create_topic, #forum
And if I need to use it in views:
can? :create_topic, #forum
by defining a custom permission on the parent object,
I'm currently trying to achieve something similiar, but I don't have the whole sight into this. Try this:
can :create, ThreadParticipation => Thread, do |participation, thread|
# your definition here
end
This should also work without a block.
EDIT: drop the part above, that doesn't work yet in CanCan. I implemented my own solution, but it requires you to authorize controller actions manually, which is not as beautify, but more secure in my opinion.
I implemented it this way for my project:
https://gist.github.com/822208
Normally you would user
user
not
current_user
within ability.rb. Is this your error? As well your ability is specified incorrectly. You want
can :create, ThreadParticipation, :user_id => user.id
Note that :user_id is a property of ThreadParticipation model