DRYing up Rails Views with Nested Resources - ruby-on-rails

What is your solution to the problem if you have a model that is both not-nested and nested, such as products:
a "Product" can belong_to say an "Event", and a Product can also just be independent.
This means I can have routes like this:
map.resources :products # /products
map.resources :events do |event|
event.resources :products # /events/1/products
end
How do you handle that in your views properly?
Note: this is for an admin panel. I want to be able to have a "Create Event" page, with a side panel for creating tickets (Product), forms, and checking who's rsvp'd. So you'd click on the "Event Tickets" side panel button, and it'd take you to /events/my-new-event/tickets. But there's also a root "Products" tab for the admin panel, which could list tickets and other random products. The 'tickets' and 'products' views look 90% the same, but the tickets will have some info about the event it belongs to.
It seems like I'd have to have views like this:
products/index.haml
products/show.haml
events/products/index.haml
events/products/show.haml
But that doesn't seem DRY. Or I could have conditionals checking to see if the product had an Event (#product.event.nil?), but then the views would be hard to understand.
How do you deal with these situations?
Thanks so much.

I recommend you to make separate admin controller with it's own views to administrate everything you want. And your customer's logic stayed in products contoller.

I don't have good and clean solution for this problem. Usualy if views doesn't differ to much, I use single view and add some code like #product.event.nil?. You can always add some variable, or helper that will make this method shorter, on example has_event? - then your view will look cleaner. And use it in code like this:
<% if has_event? %>
some html
<% end %>
or for single line:
<%= link_to 'Something special', foo_path if has_event? %>
On the other side, you can create few partials that are the same for both views, put them in some folder, on example /shared/products/... and render them from your views like this:
<%= render :partial => '/shared/products/info' %>
and so on.
But if they don't differ too much, I really would use if version.

The views will be handled by the ProductsController. You can alter the logic in your controller depending on the nesting of the resource.
# app/controller/products_controller.rb
# ...some code...
def index
#event = Event.find_by_id(params[:event_id]) if params[:event_id]
#products = #event ? #event.products : Product.all
end
The view will be handled by the usual product view
# app/views/products/index.html.haml
- unless #products.blank?
- #products.each do |product|
%p= product.some_attribute

Related

ActiveAdmin and handling a subset of data

We use ActiveAdmin and have a notion of an event at like http://domain.com/admin/events/3/edit. I'd like to be able to edit the finance data aspect of this (something like http://domain.com/admin/events/3/finance/edit or http://domain.com/admin/events/3/edit/finance). How would you organize it? I was thinking of trying to do a custom controller like this:
ActiveAdmin.register Event, as: 'Finance' do
permit_params %i(venue_id name event_type_id)
before_create do |event|
event.created_by = current_user
end
show do |event|
panel 'Details' do
attributes_table_for event do
row :id
end
end
end
end
but this doesn't seem to work. What would be the most basic pattern to have a controller which has a portion of our data and can work well within ActiveAdmin?
You appear to be asking about nesting a resource inside another, see 'Belongs To' on this page

Trying to get two actions to have different views, but the same URL

I have an app with shepherds (i.e. the users) and each shepherd has a set of animals. I am trying to make it so that if the logged in shepherd tries to view one of their animals it takes them to a view controlled by the edit action. Alternatively, if the shepherd tries to view another shepherd's animals it gives them a view controlled by the show action. I'd prefer to not have URLs with */edit - I want them to look the same for edit and show. In my routes.rb file I have this...
get '/shepherds/:username/:eartag', to: 'animals#show', as: :shepherd_animal_show
get '/shepherds/:username/:eartag', to: 'animals#edit', as: :shepherd_animal_edit
In the shepherd's show view I create a link to their animals like this...
<% if animal.shepherd == current_shepherd %>
<a href="<%= shepherd_animal_edit_path(username: animal.shepherd.username, eartag: animal.eartag) %>"
<% else %>
<a href="<%= shepherd_animal_show_path(username: animal.shepherd.username, eartag: animal.eartag) %>"
<% end %>
I can see that when I'm logged in and try to access my animals, it goes into the block with the shepherd_animal_edit_path, but it directs me to the show view rather than the edit view. Here's part of the output of the routes...
shepherd_animal_show GET /shepherds/:username/:eartag(.:format) animals#show
shepherd_animal_edit GET /shepherds/:username/:eartag(.:format) animals#edit
I'm a bit of a beginner at Rails and would really appreciate any help people can offer.
I think you should be using the show action for both, and just rendering a different template depending on the animal ownership. I.e.:
def show
...
render 'edit' if animal.shepherd == current_shepherd
# the show template will be rendered automatically if `render` wasn't explicitly called
end
Then in your view, you can unconditionally link to the show action, and the controller will take care of deciding which view template to render.
Also, FWIW, I would probably define the routes as follows:
resources :shepherds, shallow: true do
resources :animals
end
And then use the link_to helper in the view instead of hard-coding HTML tags:
<%= link_to animal_path(animal) %>
But that would then allow you to do other things like:
link_to shepherd_animals_path(#shepherd) # link to the `index` action of `AnimalsController`, with `params[:shepherd_id]` set to `#shepherd.id`.
That would then allow you to render (or not render) other controls for the shepherd to manage his/her own flock if current_shepherd.id == params[:shepherd_id] (or current_shepherd == Shepherd.find(params[:shepherd_id])).

rails best way to display button only for admin

I have an application with Post model with associated PostsController and Admin::PostsController under the admin namespace in routes. Controllers share the same index action with controller concern, similar to this approach. I'm using a shared view partial under the shared/posts/_posts_list to list all posts on site and also in the admin dashboard. All this is working as I expected.
I'm asking what is the best approach to add for instance: edit post button only for admin user, that view doesn't get bloated with conditionals like <% if current_user.admin? %> to display this edit button.
If you don't want to put conditionals in your view go for two
separate views (one for regular users, one for admins).
Use conditionals in view and
include necessary elements depending on is_admin?. This is the shortest way to go if the only conditional is is_admin? check, imho. If there are more conditionals (roles, etc.) you are right, this will bloat views. You can check redmine (it is a big, open source rails project) and see that it uses conditionals in views too. Check its base layout here.
You can use helpers to generate elements depending on is_admin?. Lets say you need to show menu elements in your view. You can generate menu elements in your helper. And you can call the helper from your view. Such as:
view.html.erb
<ul>
<%= main_menu_items("main_menu", current_user) %>
</ul>
some_helper.rb
def main_menu_items(menu_name, user = current_user)
allowed_items(menu_name, user).map { |menu_item| content_tag(:li, menu_item) }.join
end
def allowed_items(menu_name, user)
menus = {
main_menu: ["Menu Item 1", "Menu Item 2"],
main_menu_admin: ["Menu Admin Item 1"]
}
menu_to_return = menus[menu_name.to_sym]
menu_to_return += menus[(menu_name + "_admin").to_sym] if user.admin?
end
This is just some quick code. If you want to use it in production, you must make some heavy changes (refactoring, getting items from models, nil checks, etc).
I'd say you've got two approaches:
The one you mentioned. Your view shouldn't get too bloated, and this is the one I'd go with.
You could use the current_user.admin? Check to put an "is_admin" CSS class on your page body. Then, you can add "admin_only" classes to your buttons etc, and use CSS to hide the relevant buttons and stuff when no "is_admin" class exists.
Either approach is fine, but 1 is a bit simpler and immediately more obvious to anyone reading the code, so I'd start with that. Kinda depends on how many admin-only features your UI will have though... If there'll be heaps, option 2 might be worth it.
You need an authorization library(https://github.com/CanCanCommunity/cancancan)
Gem install
gem 'cancancan', '~> 1.10'
rails g cancan:ability
config
class Ability #model/ability.rb manage roles
include CanCan::Ability
def initialize(user) # user is devise current_user
if user.blank?
else
if user.role_id == 1 # role_id 1 is admin
can :manage, Post #Post is you model
end
end
end
view
<% if can? :update, #post %>
<%= link_to "Edit", edit_post_path(#post) %>
<% end %>
controller
class ArticlesController < ApplicationController
load_and_authorize_resource
def index
#posts = Post.accessible_by(current_ability).order("id ASC") #posts is filtered by current_user's roles
end
def show
# #post is already loaded and authorized
end
end

Ruby on Rails layouts and rendering

I'm new to RoR and I'm a little bit confused with Rails MWC. I feel like I misunderstand something.
For example, I want to have home page where I could render top 5 articles and top 5 products. Products and articles have no relations at all, it is totally separate data.
So what I try to do is, i crate 2 sacffolds products and articles, and 1 controller for home page. I root to homepage controller. Then in homepage template i try to render products and article template. I get an error that methods which are used in products and articles controllers are undefined.
I don't understand where is problem. Is this kind of template rendering one template inside another is not Rails convention. Or I have bugs in my code.
I don't see your code but in this case I'm quite sure you have bugs in it.
app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
#products = Product.top5 # Your logic to fetch top 5
#articles = Article.top5
end
end
app/views/home/index.html.erb
<% #products.each do |product| %>
<%= product.name %>
<% end %>
<% #articles.each do |article| %>
<%= article.name %>
<% end %>
This is perfectly fine, I've done that multiple times. Consider that in Rails you don't have any relation between controller and models, there are convention but Rails controller is not bound at all to any model
First, you need to instantiate your variables #products and #articles (this is an example) on your controller method. Then you can render the view.
Pay attention to add an # before. Only variables with an # will be available on your rendering view.
By default, when you call a GET for /products you'll arrive on the index method. At the end of this method, if not any view is specified, Rails will render views/products/index. In this view, you'll access all variables instantiate with an # and do whatever you want with.
First, yes, a template rendering another controller's template (not a partial) is not within Rails conventions.
A scaffold is a "single-resource" controller: it takes your model definition and generates a basic controller for editing and displaying that particular model (i. e., Product).
What you really need to do is use the two models you've generated in the home page controller, kinda like this:
class HomePageController < ApplicationController
def index
#articles = Article.top_5
#products = Product.top_5
# Render the #articles and #products in the view.
end
end

Rails: Create a custom UI for a single specific object

I've run into a problem I'm completely unsure how to approach.
I have an app for sharing architectural photos. Users have_many Photos, and users can create Collections which also have_many Photos.
Now I have one customer who is a big name in the industry who would like to work with me to create a totally customized Collection with a very different look and feel from "regular" collections, but essentially the same functionality underneath. I'd like to accommodate this request, but I really have no idea how to do it.
Given that I already have a functioning Collection model and CollectionsController, plus all the views, I'd like to re-use as much of that as possible. So, for instance, the custom Collection needs to override the user facing :show view, but not the admin :edit view.
How would you approach something like this?
I'm trying to understand the most efficient, DRY method for creating a completely custom UI for a single record in the database. I'd be very appreciative of suggestions, including links to articles / books etc, as I haven't been able to find much in this area.
I would allow the creation of Liquid view templates associated with a User and/or Collection (if you want both - per-user templates with per-collection variations - use a polymorphic association) and of course fall back to your default view (also built with Liquid for consistency and reference) for all cases where no custom template is found.
Edit to add suggested details:
Any custom templates should be stored in the database (I would add a test/preview function so the user entering a custom template has the chance to verify their template before publishing it):
# Table name custom_templates
# id :integer
# templatable_type :string
# templatable_id :integer
# contents :text
class CustomTemplate < ActiveRecord::Base
belongs_to :templatable, :polymorphic => true
end
class User
has_one :custom_template, :as => :templatable
end
class Collection
has_one :custom_template, :as => :templatable
end
In your controller action, look for a custom template:
custom_template = #collection.custom_template
custom_template ||= #user.custom_template
#custom_template = Liquid::Template.parse(custom_template.contents) if custom_template
In your view, either render the custom template or your default template partial:
<% if #custom_template -%>
<%= #custom_template.render(_hash_of_objects_to_pass_to_liquid_template_) %>
<% else -%>
<%= render :partial => 'default' %>
<% end -%>

Resources