need assistance with some ruby array code, please - ruby-on-rails

I am making an app where current_user (logged in user) can write reviews, and make each review public or private,
with a radio button.
If public, every other user can see that review. If private, only current_user can see it.
visible.true and visible.false, depending on which radio button is selected.
I'm trying to come up with the code/syntax to get this working?
Something like:
#review.user is the person who wrote a particular review
#if review.user is not current_user, and the review is
#marked as false, then don't show that review
If review.user != current_user
&& review.visible = false
don't show review.
At present in a reviews_helper.erb I have:
def review_block(review, options = {})
options = {:review => review}
render 'reviews/review', options
end
And in my view, show.html.erb:
<div class="reviews" >
<% #reviews.each do |review| %>
<%= review_block review %>
<% end %>
</div>
Any chance you could tell me how I should modify my helper to get it working, or any other suggestions?

In the case you want to filter the #reviews array you could do something like this:
#reviews.select { |review| review.visible_to?(current_user) }.each do |review|
render 'reviews/review', :review => review
end
The Array's #select method filters a given array with the condition passed as block. I would move the visibility logic to the Review model to the method call visible_to? which would be something like you said above:
# review.rb
def visible_to?(user)
self.user.id == user.id || # assuming they have an ID
visible == true
end
Better yet, if you are using Rails you can completely remove the select method call from the view and create a scope in the Review class.
Edit: Using a scope
#review.rb
scope :visible_to, lambda { |user| conditions( 'user_id = ? or visible = ?', user.id, true ) }
This way, when you are building your #reviews array - presumably in a controller action, you can do something like this:
#reviews_controller.rb
#reviews = Review.visible_to(current_user)
You can obviously nest several scopes - like order, limit, where, etc - and filter the review the way you want. Nevertheless the utility visible_to? method should also be defined for the instance itself alongside with the scope.
Always keep in mind to have your views as dumber as you can, i.e. your views should know the least about your models and your business logic. This will ensure there are no tight dependencies between your views and your models.

Related

looping over records with condition

I have a Customer model that has the attribute private. This attribute is only visible for a total of 3 users in the database. If this method (check_box) gets checked by one of those 3 users the Customer is only visible by them.
I'm currently looping over all of the Customers like this:
<% #customers.where(:private => false).each do |single_customer| %>
My question is how can I accomplish when one of the 3 users is signed in that :private => false gets changed to #customers.each do |single_customer| because then I don't want to filter the private attribute anymore.
you need to change the build up of the loop. This snippet is a bit of meta programming, but you can do it like this:
# in the controller for example.
#customers = Customer.where(private: false)
#customers = Customer.all if current_user.is_my_special_user?
In the view you then simply do this: <% #customers.find_each do |customer| %>
use find_each for better performance if your collection is huge.
by default you use the private: false
if you detect your user is logged in, you overwrite the #customers

How do I use a button to update the order of a filtered category?

I am new to Rails, but slowly making progress. I can't quite wrap my head around how to achieve my next task.
I have a controller (IdeasController) with an index that looks like this:
def index
if params[:round].blank? && params[:challenge].blank?
#ideas = Idea.all.order(params[:sort])
# #ideas = Idea.all.order(created_at: :desc, cached_votes_up: :desc)
end
if params[:round].present?
#round_id = Round.find_by(name: params[:round]).id
#ideas = Idea.where(round_id: #round_id).order("created_at DESC")
end
if params[:challenge].present?
#challenge_id = Challenge.find_by(name: params[:challenge]).id
#ideas = Idea.where(challenge_id: #challenge_id).order("created_at DESC")
end
end
I am updating the view and filtering by category with the above :round and :challenge with the code below in my index.html.erb:
<%= link_to "All", ideas_path %>
<% Round.all.each do |round| %>
<%= link_to round.name, ideas_path(round: round.name) %>
<% end %>
<% Challenge.all.each do |challenge| %>
<%= link_to challenge.name, ideas_path(challenge: challenge.name) %>
<% end %>
Now, my problem is that I want to create a button that orders by created_at DESC or ASC. I want the button to essentially be a toggle. I also want another button to order by cached_weighted_average DESC or ASC. This is from acts_as_votable so I can sort by vote counts.
The problem I am running into is that I can create a link or button that orders by created_at or cached_weighted_average, but it replaces all of the URL that was previously filtered by :round or :challenge. For example, if a user clicks "Round 1" and sees all ideas marked for "Round 1" and then they click the link to order by cached_weighted_average, the URL replaces:
/ideas?round=Round+1
With this:
/ideas?sort=cached_weighted_average+ASC
What I want is:
/ideas?round=Round+1&?sort=cached_weighted_average+ASC
I know this is a very new question, but everything I have tried has failed so far. It feels like I am missing something very easy. What I noticed I can do easily is inside the controller I can do something like:
if params[:round].present?
#round_id = Round.find_by(name: params[:round]).id
#ideas = Idea.where(round_id: #round_id).order("cached_weighted_average DESC")
end
Which is perfect. This button just needs to switch between cached_weighted_average DESC and created_at DESC.
Any help is appreciated, thanks.
passing multiple parameters is one way to handle:
<%= link_to object.name, object_path(first: something, second: something_else) %>
then alter your conditionals to contemplate presence of multiple params.
to differentiate between round and challenge when attempting to allow the user to choose how they'd like to sort you could use the same name and then pass it different values.
something like:
params["round_or_challenge"]
this would change your conditional to something like:
if params["round_or_challenge"] == "round" && params["asc_or_desc"] == "asc"
# query
elsif params["round_or_challenge"] == "challenge"
# query
end
or whatever. it's basically the same...just pass the values you need. you can also pass the existing parameters from the view the same way you access them in the controller.
Thanks for the response, #toddmetheny. I didn't implement your solution, but your solution helped me understand passing multiple parameters a bit more.
I ended up creating a helper, sortable. I also used the url_for to append at the end of whatever the current URL might be. I liked this approach because it meant I could sort on any parameter. I'm not sure that it's the best solution, but it works.
def sortable (name, sort)
link_to name, url_for(params.merge(sort: sort))
end

Rails multiple records select and apply multiple actions

I am quite new at rails and I am having some trouble designing an admin dashboard.
What I want to achieve is this:
Have a list of multiple users from database.
Have the ability to select multiple records.
Have the ability to apply different actions to all of the
selected records.
The actions MAY not be directly translatable into SQL queries. (for example send an email)
I am not looking for a complete solution to the problem just a general description on how to approach this. I have a feeling I started on a wrong path.
So far I am doing this:
view
<%= form_tag("some_path", method: "get") do %>
<% #users.each do |user| %>
<%= check_box_tag "user_ids[]", users.id %>
<%end%>
<%= submit_tag("Send Email") %>
<%end%>
controller
def send_email
#recipients = User.find(params[:user_ids])
#recipients.each do |recipient|
Notifier.raw_email(recipient.email, params[:user_email][:subject], params[:user_email][:body]).deliver
end
end
This works as it is but i can only apply one action, send email that is.
I want to be able to choose an action to apply to all selected records or apply multiple actions to the selected records
Any thoughts?
You can use the send method to call methods of the model.
class User
def send_email(subject, body)
Notifier.raw_email(self.email, subject, body).deliver
end
end
Let /some_path also accept an array of actions
In our case actions = ['send_email']
In the action that some_path resolves to,
class SomeController < ActionController::Base
def some_action # that some_path resolves to in your config/routes.rb
#recipients = User.find(params[:user_ids])
#actions = params[:actions]
#recipients.each do |recipient|
#actions.each do |action|
recipient.send(action, params[:subject], params[:body])
end
end
end
end
In this way you can call multiple methods. Make sure you only accept valid action values or else the admin can simply call any of the User's methods.
You can have a select tag with the different actions you need.
Then on change of the select tag, you can update the action attribute of the form. eg, using jQuery.
$('#my-action-select').change(function() {
$('#myform').attr('action', $(this).val)
})

excluding a value from a form drop down in rails

I have a form drop down, that shows me all the emails in my User db table.
<%= f.collection_select(:accessor_id, User.all,:email ,:email) %>
I want to exclude from this list the value of the current's user email, which I can find with with current_user.email (already defined and working)
I know I can achieve this via the following query:
<%= f.collection_select(:accessor_id, User.select(:email).where("email !=?" , current_user.email),:email ,:email) %>
i wanted to know if it is possible to do this after User.all returned all of the values.
you mean something like
User.all.reject {|user| user == current_user}
or more precisely i would fetch all users somewhere in the controller
def index
#users = User.all
end
and use something like that in the form
<%= f.collection_select(:accessor_id, #users.reject {|user| user == current_user}.map(&:email)) %>
#phoet answer is correct, personally I would probably do this at database level anyhow, something along lines of
class User < ActiveRecord::Base
# ...
def self.all_without(excluded)
where("id NOT IN (?)", excluded)
end
end
<%= f.collection_select(:accessor_id, User.all_without([current_user]), :email ,:email) %>
try to keep the view 'clean' of the details, if possible
# and if you really do only want to pull email from the database, you can chain the query
User.all_without([current_user]).select(:email)

How to hide parts of the view given a user role on Rails 4

I'm trying to hide parts of my views depending on the User role.
So let's say I want only admins to be able to destroy Products. Besides the code in the controller for preventing regular users from destroying records, I would do the following in the view:
<% if current_user.admin? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
The previous code works, but it's prone to errors of omission, which may cause regular users to see links to actions they are not allowed to execute.
Also, if I decide later on that a new role (e.g. "moderator") can delete Products, I would have to find the views that display a delete link and add the logic allowing moderators to see it.
And if there are many models that can be deleted only by admin users (e.g. Promotion, User) maitenance of all the ifs would be pretty challenging.
Is there a better way of doing it? Maybe using helpers, or something similar? I'm looking for something maybe like this:
<%= destroy_link 'Delete', product %> # Only admins can see it
<%= edit_link 'Edit', promotion %> # Again, only admins see this link
<%= show_link 'Show', comment %> # Everyone sees this one
I found these two questions that are similar to mine, but none of them answered my question:
Show and hide based on user role in rails
Ruby on Rails (3) hiding parts of the view
I strongly recommend pundit.
It allows you to create "policies" for each model. For your Product model you might have a ProductPolicy that looks something like this
class ProductPolicy < ApplicationPolicy
def delete?
user.admin?
end
end
In your view you can do something like this
<% if policy(#post).delete? %>
<%= link_to 'Delete', product, method: :delete %>
<% end %>
If later on you want to add a moderator role, just modify the policy method
class ProductPolicy < ApplicationPolicy
def delete?
user.admin? || user.moderator?
end
end
So I kind of figured a way to move the IFs out of the view. First, I override the link_to helper in my application_helper.rb:
def link_to(text, path, options={})
super(text, path, options) unless options[:admin] and !current_user.admin?
end
Then on my views I use it as:
<%= link_to 'Edit Product', product, admin: true, ... %>
This prevents regular users from seeing admin links, but for other html tags with content inside, such as divs, tables etc., an if would still be needed.
CanCan is another gem that lets you define "Abilities" per user role.
In views you can use something like if can? :delete, #post to check if the
user may delete that specific post.
Using the CanCan and Role gems, what is still needed is a way to Check The Route and see if "current_user" has permissions to access that Route based on their role(s) - then show/hide based on that.
This saves the user clicking on things and getting told they cannot see it - or us having to write per-item "if" logic specifying what roles can see what list-items (which the customer will change periodically, as roles are changed/refined) around every single link in one's menu (consider a bootstrap menu with 50+ items nested in groups with html formatting, etc), which is insane.
If we must put if-logic around each menu-item, let's use the exact same logic for every item by checking the role/permissions we already defined in the Ability file.
But in our menu-list, we have route-helpers - not "controller/method" info, so how to test the user's ability to hit the controller-action specified for the "path" in each link?
To get the controller and method (action) of a path (my examples use the 'users_path' route-helper) ...
Rails.application.routes.recognize_path(app.users_path)
=> {:controller=>"users", :action=>"index"}
Get just the controller-name
Rails.application.routes.recognize_path(app.users_path)[:controller]
=> "users"
Ability uses the Model for its breakdown, so convert from controller name to it's model (assuming default naming used) ...
Rails.application.routes.recognize_path(app.users_path)[:controller].classify
=> "User"
Get just the action-name
Rails.application.routes.recognize_path(app.users_path)[:action]
=> "index"
And since the "can?" method needs a Symbol for the action, and Constant for the model, for each menu-item we get this:
path_hash = Rails.application.routes.recognize_path(app.users_path)
model = path_hash[:controller].classify.constantize
action = path_hash[:action].to_sym
Then use our existing Abilty system to check if the current_user can access it, we have to pass the action as a symbol and the Model as a constant, so ...
<% if can? action model %>
<%= link_to "Users List", users_path %>
<% end %>
Now we can change who can see this resource and link from the Ability file, without ever messing with the menu, again. But to make this a bit cleaner, I extracted out the lookup for each menu-item with this in the app-controller:
def get_path_parts(path)
path_hash = Rails.application.routes.recognize_path(path)
model_name = path_hash[:controller].classify.constantize
action_name = path_hash[:action].to_sym
return [model_name, action_name]
end
helper_method :get_path_parts
... so I could do this in the view (I took out all the html-formatting from the links for simplicity, here):
<% path_parts = get_path_parts(users_path); if can?(path_parts[1], path_parts[0]) %>
<%= link_to "Users Listing", users_path %>
<% end %>
... and to make this not take all day typing these per-menu-item if-wraps, I used regex find/replace with capture and wildcards to wrap this around every list-item in the menu-item listing in one pass.
It's far from ideal, and I could do a lot more to make it much better, but I don't have spare-time to write the rest of this missing-piece of the Role/CanCan system. I hope this part helps someone out.

Resources