Implementing datagrid gem & session helper methods - ruby-on-rails

I'm a newbie trying to implement the datagrid gem (https://github.com/bogdan/datagrid). Unfortunately, I'm getting three error messages:
Error 1: undefined method 'filter' for #<UsersGrid:0x000000044e9400>
Referring to: line `#grid.filter` in def index
Error 2: undefined local variable or method 'user' for #<UsersController:0x007f821f434808>
Referring to: line `link_to view_context.image_tag("delete.gif",...` in the controller.
Error 3: undefined method `image_tag' for UsersGrid:Class
Referring to: line `link_to image_tag(user.avatar.url,` in users_grid.rb
Placing `view_context` in front of link_to in users_grid.rb doesn't work either: undefined local variable or method 'view_context' for UsersGrid:Class.
Also tried `ActionController::Base.helpers.image_tag`. Although that seems to solve the image_tag issue, I then get the error message: `undefined method 'user_path' for #<ActionView::Base:0x007f821d3115b8>` referring to that same line.
Removing all the lines above, and the form works :-)
Any suggestions how to change the code below to adjust for these errors?
My code: After installing the gem I have created /app/grids/users_grid.rb:
class UsersGrid
include Datagrid
scope do
User.order("users.created_at desc")
end
filter(:id, :integer)
filter(:email, :string) { |value| where('email like ?', "%#{value}%") }
filter(:organization, :string) { |value| where('organization like ?', "%#{value}%") }
filter(:verified, :xboolean)
filter(:created_at, :date, :range => true, :default => proc { [1.month.ago.to_date, Date.today]})
column(:id)
column(:avatar) do |user|
if user.avatar?
link_to image_tag(user.avatar.url, alt: "Profile"), user_path(user) #Error3
else
link_to image_tag("profile.gif", alt: "Profile"), user_path(user)
end
end
column(:organization)
column(:verified) do |user|
image_tag("verifiedaccount.gif", title: "verified") if user.verified
end
column(:created_at) do |model|
model.created_at.to_date
end
end
The users controller:
def index
#grid = UsersGrid.new(params[:users_grid]) do |scope|
scope.where(admin: false).page(params[:page]).per_page(30)
end
#grid.assets
if (current_user && current_user.admin?) # This is a Sessions helper method.
#grid.filter(:activated, :xboolean, :default => true) #Error1
#grid.column(:myadmin, :header => "My admin?") do |user|
view_context.image_tag("adminamina.png", title: "admin") if user.myadmin
end
#grid.column(:activated) do |user|
user.activated_at.strftime('%v') if user.activated
end
#grid.column(:remove) do |user|
link_to view_context.image_tag("delete.gif", title: "remove"), user, method: :delete,
data: { confirm: "Please confirm" } #Error2
end
end
end
The view:
<%= datagrid_form_for #grid, :method => :get, :url => users_path %>
<%= will_paginate(#grid.assets) %>
<%= datagrid_table(#grid) %>
<%= will_paginate(#grid.assets) %>

This DataGrid seems to be set up more or less like a Model class, and I think that's an important hint that they're serious about object orientation and encapsulation: the innards of the class won't automatically have access to the outside world, you need to manually specify the info you need.
One technique called dependency injection might be useful here, and fortunately DataGrid appears to have decent support for it. Quoted from this wiki page:
To pass an object to the Grid instance, simply merge it to the params hash on initialization:
grid = TheGrid.new(params[:the_grid].merge(current_user: current_user))
You can then access it in the Grid object by declaring the corresponding getter
def TheGrid
...
attr_accessor :current_user
...
end
So, when you new up the UserGrid object, specify current_user: current_user as part of the hash, then the grid should have access to it.
Then you'll have one more problem: the UserGrid object will have access to current_user, but the UserGrid class won't, and you've written your conditional in at the class level. Again the wiki has an answer for this, it's called dynamic columns and you can click that link to see a couple examples.
A brief dynamic columns example: in the controller (or wherever you create your UserGrid), try something like the following (again, more examples at the above link):
user_grid = UserGrid.new(same_params_as_before)
# The Grid object has been created, but we can still add columns to it after-the-fact
if current_user && current_user.admin?
# This block will only execute if the above condition is met. We can
# define as many extra columns as we feel like here (or change the
# user_grid object in any other way we want)
user_grid.column(:myadmin, header: "My admin?") do |user|
image_tag("adminamina.png", title: "admin") if user.myadmin
end
...
end
Some notes about this example:
The UserGrid object doesn't even need to know about current_user. It doesn't care that you're an admin; the controller simply decides whether you're an admin, and if so, makes some changes to the UserGrid object.
Consequently, you can get rid of the current_user method definition inside the UserGrid class, plus all code that references it.
This is an example of bad object-oriented programming because the controller is tasked with managing the internal state of another object (ie. manually defining more columns on the UsersGrid in certain conditions). I like this approach because it's direct and easier to understand, but be aware that there are better ways to organize this code so that all of the UserGrid "column definition stuff" is kept in the same place, not scattered around different files.

Related

Is using a helper method in a model possible and, more importantly, appropriate?

So I have the following line of code in a html.erb:
<%= "Last seen #{distance_of_time_in_words(Time.now.localtime, member.last_sign_in_at.localtime, true, :highest_measure_only => true)} ago" %>
And I'd like to simplify it with something like:
<%= member.last_seen %>
Without thinking much I tried to write an instance method for the class Member like this:
def last_seen
if last_sign_in_at.nil?
"User hasn't signed in yet"
else
"Last seen #{distance_of_time_in_words(Time.now.localtime, self.last_sign_in_at.localtime, true, :highest_measure_only => true)} ago"
end
end
It doesn't work, however, since I can't access the helper method: distance_of_time_in_words in the Models.
What'd be the appropriate way to make <%= member.last_seen %> work.
Models aren't the appropriate place to put presentation logic, which is what this is. This is what helper methods are for. A good rule of thumb is if it's not raw data coming out of a model, you're mixing presentation with domain.
It would be more appropriate to do something like:
# app/helpers/member_helper.rb
module MemberHelper
def last_seen(member)
if member.last_sign_in_at.nil?
"User hasn't signed in yet"
else
"Last seen #{distance_of_time_in_words(Time.now.localtime, member.last_sign_in_at.localtime, true, :highest_measure_only => true)} ago"
end
end
end
But to be honest, unless you're using something like this on a ton of pages in varying contexts, moving this code to a helper may be overkill vs leaving it in the view.

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.

How to create multiple "has_many through" associations through one form?

I'm building a martial arts related database, currently I have the following associations set up:
Student has_and_belongs_to_many :styles
Style has_many :ranks
Student has_many :ranks, through: :gradings (and vice versa)
I'm generating a form as follows, depending on the student's styles:
So the headings are generated by the Style model (Tai Chi, Karate...), then their rankings listed below (taken from the Rank model), and the "Dojo" and "Date" fields should belong to the Grading model once created.
The question: I know how to build a form that creates one association (or one association + its children), but how do I build a form that creates multiple associations at once?
Also, what would be a clean way to implement the following:
Only lines which are ticked become associations
Dojo and date must be filled in for ticked lines to save successfully
If a line is unticked it will destroy any previously created associations
This is what I've currently implemented to retrieve the correct records:
class GradingsController < ApplicationController
before_filter :authenticate_sensei!
def index
#student = Student.includes(:styles).find(params[:student_id])
#ranks = Rank.for_student_styles(#student)
split_ranks_by_style
end
private
def split_ranks_by_style
#karate = #ranks.select_style("Karate")
#tai_chi = #ranks.select_style("Tai Chi")
#weaponry = #ranks.select_style("Weaponry")
end
end
# Rank model
def self.for_student_styles(student)
includes(:style).where("styles.id in (?)", student.styles.map(&:id))
end
def self.select_style(style)
all.map { |r| r if r.style.name == style }.compact
end
Complicated forms like this are best handled in a service object initiated in the primary resource's create or update action. This allows you to easily find where the logic is happening afterwards. In this case it looks like you can kick off your service object in your GradingsController. I also prefer formatting a lot of the data in the markup, to make the handling easier in the service object. This can be done a'la rails, by passing a name like "grade[style]" and "grade[rank]". This will format your params coming in as a convenient hash: {grade: {style: "karate", rank: "3"}}. That hash can be passed to your service object to be parsed through.
Without really grasping the full extent of your specific requirements, let's put together an example form:
<%= form_for :grading, url: gradings_path do |f| %>
<h1><%= #rank.name %></h1>
<%- #grades.each do |grade| %>
<div>
<%= hidden_field_tag "grade[#{grade.id}][id]", grade.id %>
<%= check_box_tag "grade[#{grade.id}][active]" %>
...
<%= text_field_tag "grade[#{grade.id}][date]" %>
</div>
<%- end %>
<%= submit_tag %>
<%- end %>
With a form like this, you get your params coming into the controller looking something like this:
"grade"=>{
"1"=>{"id"=>"1", "active"=>"1", "date"=>"2013-06-21"},
"3"=>{"id"=>"3", "date"=>"2013-07-01"}
}
Nicely formatted for us to hand off to our service object. Keeping our controller nice and clean:
class GradingsController < ApplicationController
def index
# ...
end
def create
builder = GradeBuilder.new(current_user, params['grade'])
if builder.run
redirect_to gradings_path
else
flash[:error] = 'Something went wrong!' # maybe even builder.error_message
render :action => :index
end
end
end
So now we just need to put any custom logic into our builder, I'd probably recommend just making a simple ruby class in your /lib directory. It could look something like this:
class GradeBuilder
attr_reader :data, :user
def self.initialize(user, params={})
#user = user
#data = params.values.select{|param| param['active'].present? }
end
def run
grades = data.each{|entry| build_grade(entry)}
return false if grades.empty?
end
private
def build_grade(entry)
grade = Grade.find(entry['id'])
rank = grade.rankings.create(student_id: user, date: entry['date'])
end
end
There will obviously need a lot more work to pass all the specific data you need from the form, and extra logic in the GradeBuilder to handle edge cases, but this will give you a framework to handle this problem in a maintainable and extensible way.

need assistance with some ruby array code, please

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.

Dealing with nil in views (ie nil author in #post.author.name)

I want to show a post author's name; <% #post.author.name %> works unless author is nil. So I either use unless #post.author.nil? or add a author_name method that checks for nil as in <% #post.author_name %>. The latter I try to avoid.
The problem is that I may need to add/remove words depending on whether there is a value or not. For instance, "Posted on 1/2/3 by " would be the content if I simply display nil. I need to remove the " by " if author is nil.
Null object pattern is one way to avoid this. In your class:
def author
super || build_author
end
This way you will get an empty author no matter what. However, since you don't actually want to have an empty object sometimes when you do expect nil, you can use presenter of some kind.
class PostPresenter
def initialize(post)
#post = post
end
def post_author
(#post.author && #post.author.name) || 'Anonymous'
end
end
Another way is using try, as in #post.author.try(:name), if you can get used to that.
You can use try:
<%= #post.author.try(:name) %>
It will attempt to call the name method on #post.author if it is non-nil. Otherwise it will return nil, and no exception will be raised.
Answer to your second question: In principle there is nothing wrong with the following:
<% if #post.author %>
written by <%= #post.author.name %>
<% end %>
or
<%= "written by #{#post.author.name}" if #post.author %>
But if this is a recurring pattern, you might want to write a helper method for it.
# app/helpers/authors_helper.rb or app/helpers/people_helper.rb
class AuthorsHelper
def written_by(author)
"written by #{author.name}" if author
end
end
# in your views
<%= written_by(#post.author) %>
Write a method which accepts any variable and checks to see if it is nuil first, and if it is not displays it. Then you only have to write one method.
I found your question interesting as I have often come across similar situations, so I thought I'd try out making my first Rails plugin.
I'm afraid I haven't put in any tests yet but you can try it out http://github.com/reubenmallaby/acts_as_nothing (I'm using Ruby 1.9.1 so let me know if you get any problems in the comments or on Github!)

Resources