ActiveResource multiple nested routes for model - ruby-on-rails

I'm using ActiveResource to pull objects from an internal API
This API has the following(simplified):
class Project < ApplicationRecord
has_many :contributions
...
end
class Contribution < ApplicationRecord
belongs_to :project
belongs_to :user
...
end
class User < ApplicationRecord
has_many :contributions
...
end
and routes so contributions can only be created associated to a project
resources :projects, except: [:new, :edit] do
resources :contributions, except: [:new, :edit, :destroy]
end
resources :users, except: [:new, :edit] do
resources :contributions, only: [:index, :show]
end
resources :contributions, only: [:index, :show, :update]
Is it possible to submit a dynamic prefix so that I can hit these paths selectively? i.e. projects/:project_id/contributions on create, but /contributions on index (all).
EDIT:
My active resources all look like so:
class Contribution < ActiveResource::Base
self.site = "#{base_url}/api/v1/"
self.headers['Authorization'] = "Token token=\"#{TokenGenerator}\""
...
end
Not much customization there.
My biggest concern is the create post which I would like to always be nested inside a project.
At the moment I'm checking params in the /contributions route to see if there is any viable 'parent_id' in them, and figuring out if said parent exists.
I'm guessing the gem was not designed with the idea of a resource having multiple routes. I can always include:
class Project ActiveResource::Base
self.site = "#{base_url}/api/v1/"
self.headers['Authorization'] = "Token token=\"#{TokenGenerator}\""
...
def contributions
Contributions.all(params: {project_id: id})
end
end
inside Projects.rb and make sure the API controller knows how to handle parents if they exist, but only because I have access to the source of both the API and the consumer app.
worth asking too: Am I just over complicating this?

Related

How to add the name of a blog post's category to route url with Rails 7

I have been trying to add the name of the BlogCategory that a BlogPost belongs to in a URL such as this:
sitename.com/blog/category-name/blog-post-title
At the very least, I want this to render for the show of the BlogPost but am okay with it being the url for every action such as new, edit, and destroy.
I'm using the friendly_id gem, if that makes a difference.
BlogCategory Model:
class BlogCategory < ApplicationRecord
extend FriendlyId
friendly_id :name, use: :slugged
has_many :blog_posts
# This is a self referential relation. This is where records in a table may point to other records in the same table.
has_many :sub_categories, class_name: "BlogCategory", foreign_key: :parent_id
has_many :sub_category_blog_posts, through: :sub_categories, source: :blog_posts
belongs_to :parent, class_name: 'BlogCategory', foreign_key: :parent_id, optional: true
# This is a scope to load the top level categories and eager-load their posts, subcategories, and the subcategories' posts too.
scope :top_level, -> { where(parent_id: nil).includes :blog_posts, sub_categories: :blog_posts }
def should_generate_new_friendly_id?
slug.nil? || name_changed?
end
end
BlogCategory Controller:
class BlogCategoriesController < ApplicationController
before_action :admin_user, only: [:new, :create, :edit, :update, :destroy]
before_action :set_blog_link, only: [:show, :edit, :update, :destroy]
...
private
def cat_params
params.require(:blog_category).permit(:name, :parent_id, :sub_category, :summary)
end
def main_cat
#cat = BlogCategory.parent_id.nil?
end
def set_blog_link
#blog_link = BlogCategory.friendly.find(params[:id])
redirect_to action: action_name, id: #blog_link.friendly_id, status: 301 unless #blog_link.friendly_id == params[:id]
end
end
BlogPost Model:
class BlogPost < ApplicationRecord
extend FriendlyId
friendly_id :title, use: :history
belongs_to :blog_category
validates :title, presence: true, length: { minimum: 5 }
validates :summary, uniqueness: true
default_scope {order(created_at: :desc)}
def should_generate_new_friendly_id?
slug.nil? || title_changed?
end
end
BlogPost Controller:
class BlogPostsController < ApplicationController
before_action :admin_user, only: [:new, :create, :edit, :update, :destroy]
before_action :set_post_link, only: [:show, :edit, :update, :destroy]
before_action :find_post, only: :show
...
private
def post_params
params.require(:blog_post).permit(:title, :body, :summary, :thumbnail_link, :blog_category_id)
end
def find_post
#post = BlogPost.friendly.find(params[:id])
# If an old id or a numeric id was used to find the record, then
# the request path will not match the post_path, and we should do
# a 301 redirect that uses the current friendly id.
if request.path != blog_post_path(#post)
return redirect_to #post, :status => :moved_permanently
end
end
def admin_user
redirect_to(root_url) unless current_user.admin?
end
def set_post_link
#post_link = BlogPost.friendly.find(params[:id])
redirect_to action: action_name, id: #post_link.friendly_id, status: 301 unless #post_link.friendly_id == params[:id]
end
end
Here is relevant code from my routes.rb file:
Rails.application.routes.draw do
resources :blog_categories, path: 'blog'
resources :blog_posts
end
What I've tried
I've tried the following without success but have little understanding of what I'm doing:
get 'blog/:blog_category_name/:blog_post_title', to: 'blog_posts#show', as: 'blog_post'
and also tried
resources :blog_posts, path: 'blog/:blog_category_name/:blog_post_title', except: [:new, :create]
resources :blog_posts, only: [:new, :create]
With this in my BlogPost controller inside the show method/block:
#post_url = BlogPost.find_by(title: params[:blog_post_title], blog_category_id: params[:blog_category_name])
I even tried adding the params used in the routes to the permitted list under post_params.
I also tried making a new post to see if old posts weren't linking properly because of the url structure change.
The URL's I'm getting are not utilizing the parameters I'm passing to them.
What you're doing here is really just a nested resource but with a vanity route and and slugging which doesn't actually require such a heavy hand.
The typical controller for a nested resource would look like this:
class BlogPostsController < ApplicationController
before_action :set_blog_category
before_action :set_blog, only: [:show, :edit, :update, :delete]
# GET /blog/foo/bar - your custom vanity route
# the conventional route would be
# GET /blog_categories/foo/blog_posts/bar
def show
end
# GET /blog/foo/blogs_posts -> index
# GET /blog/foo/blogs_posts/new -> new
# POST /blog/foo/blogs_posts -> create
# ...
private
def set_blog_category
#blog_category = BlogCategory.friendly.find(params[:blog_category_id])
end
def set_blog
#blog = Blog.friendly.find(params[:id])
end
end
Besides the fact that you're using friendly.find you don't actually need to do anything to do the lookup via slugs instead of the id column. If you want to find the records only by their friendly id (and not allow numerical ids) use the find_by_friendly_id method instead.
Note that :id (or _id) in a parameter name is not equal to the id column - it's just a name for the unique indentifier segment in the URI pattern.
While you can configure the name of the param its actually kind of silly as in Rails things just work when you stick with the conventions.
You can just define the vanity route for this as:
resources :blog_categories, path: 'blog', only: [] do
# the typical routes nested under "blog_posts"
resources :blogs_posts, only: [:new, :create]
# your custom vanity route should be defined last to avoid conflicts
resources :blogs_posts, path: '/', only: :show
end
Generating the URL can be done either by calling the named blog_category_blog_path helper or by using the polymorphic route helpers:
blog_category_blog_path(#blog_category, #blog_post)
redirect_to [#blog_category, #blog_post]
form_with model: [#blog_category, #blog_post]
If you have legacy URLs using a different structure that you want to redirect I would consider using a separate controller or just doing the redirect in the routes to separate out the responsibilities from this controller.
You also should avoid duplicating the authorization/authentication logic across your controllers (your admin_user method). Thats how you get security holes.

Is there a clean way to find a polymorphic instance of an object in a nested controller?

I'm working with a nested controller using models with a polymorphic relations. I was wondering if there is a clean way to to find #holder. The code in CommentsController#set_holder is ugly and I was wondering if rails provides helpers for this issue.
class Comment < ActiveRecord::Base
belongs_to :holder, polymorphic: true
end
class Product < ActiveRecord::Base
has_many :comments, as: :holder
end
class User < ActiveRecord::Base
has_many :comments, as: :holder
end
Dummy::Application.routes.draw do
resources :products do
resources :comments
end
resources :users do
resources :comments
end
end
class CommentsController < ApplicationController
before_action :set_holder, only: [:new, :create]
# code ...
def new
#comment = #holder.comments.build
end
# code ...
private
def set_holder
# params = {"controller"=>"comments", "action"=>"new", "user_id"=>"3"}
# or
# params = {"controller"=>"comments", "action"=>"new", "product_id"=>"3"}
# Is there a Rails way to set #holder?
type, type_id = params.find { |k,_| /_id$/ === k }
#holder = type.sub(/_id$/, '').classify.constantize.find(type_id)
end
end
You can try with:
resource, id = request.path.split('/')[1, 2]
#holder = resource.classify.constantize.find(id)
Also in routes you can make things shorter by:
resources :users, :products do
resources :comments
end

How do I write the routes for these resources?

For my rails application the associations are as follows:
A user has many bookmarks and belongs to user.
A user has many friendships.
A user has many reminders.
A user has many comments.
A bookmark has many comments.
A comment belongs to a user and belongs to a bookmark.
A friendship belongs to a user.
A reminder belongs to a user
My routes.rb file:
Rails.application.routes.draw do
root 'welcome#index'
get 'home', :to => 'home#index'
get 'searchApis', :to => 'home#searchApis'
devise_for :users, :controllers => { registrations: 'registrations' }
resources :users, shallow: true do
resources :bookmarks, except: :new
resources :friendships, only: [:index, :show, :destroy]
resources :reminders
end
resources :bookmarks, shallow: true do
resources :comments
end
end
Am I writing these routes out correctly?
When I rake routes, I'm getting bookmarks#index twice so I am confused. The prefix for one of them is bookmark, and the other is bookmarks. Why is this happening?
From my understanding, the application does not need to see all of the bookmarks in an index because they are only visible to the user who made them. However, I want reminders to be visible to the user's friends.
I'm hoping to get suggestions to clean up my routes, if possible. I really doubt I am doing this correctly.
My interpretation of your spec:
#config/routes.rb
resources :users, only: [] do #-> show a user's collections (no edit)
resources :bookmarks, shallow: true, except: [:new, :edit, :update] #-> url.com/bookmarks/:id
resources :comments, :friendships, :reminders, shallow: true, only: [:index, :show] #-> url.com/comments/:id
end
resource :bookmarks, except: :index do #-> url.com/bookmarks/:id
resources :comments #-> url.com/bookmarks/:bookmark_id/comments/:id -- should be scoped around current_user
end
For the comments controller, do this:
#app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def new
#bookmark = Bookmark.find params[:bookmark_id]
#comment = #bookmark.comments.new
end
def create
#bookmark = Bookmark.find params[:bookmark_id]
#comment = #bookmark.comments.new bookmark_params
#comment.user = current_user
#comment.save
end
end
Don't make a welcome or home controller, you don't need them.
You can put off-hand actions in your application controller:
#config/routes.rb
root 'application#index'
get 'home', to: 'application#home'
get 'search_apis', to: 'application#search_apis'
Of course this is somewhat of an antipattern (you'll end up bloating your ApplicationController), but if you only have obscure "one-off" actions in other controllers, you'll be best suited using the above.
Also, only use snake_case with lowercase for URL's - HTTP's spec determines all URLs should be handled as lowercase:
Converting the scheme and host to lower case. The scheme and host
components of the URL are case-insensitive. Most normalizers will
convert them to lowercase. Example: →
http://www.example.com/
Although that's only for the domain/host, it's applicable in the URL itself, too.
Shallow provides the :index, :new and :create only. So you're getting index twice. Once from within users and the other bookmarks - comments.
On re-reading your associations at the start of your post, and that comments belongs_to both users AND bookmarks, it's probably a good idea to create a Polymorphic relationship.
A rough guide would be for your models,
class Comment < ActiveRecord::Base
belongs_to :messages, polymorphic: true
end
class User < ActiveRecord::Base
has_many :comments, as: :messages
end
class Bookmark < ActiveRecord::Base
has_many :comments, as: :messages
Then either rails generate migration Comments if you haven't already, and have it look like the following:
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :name
t.references :messages, polymorphic: true, index: true
t.timestamps null: false
end
end
end
otherwise run a migration to add columns to your Comment model. I.e rails g migration AddMessagesToComments messages:references
But be sure to open your new migration file named as above and add polymorphic: true before you rake db:migrate

Nested routing and authorization using CanCanCan in Rails

There is the following routing:
resources :accounts, only: [:update] do
get 'search', on: :collection
resources :transactions, only: [:create]
end
Abilities:
can [:update, :search], Account
can [:create, :index], Transaction
Controller:
# Web API controller for actions on Transaction
class Api::V1::Web::TransactionsController < Api::V1::Web::ApplicationController
load_and_authorize_resource :account
load_and_authorize_resource :transaction, through: :account
def create
render json: params and return
end
end
When I try to create a new transaction I get an error:
CanCan::AccessDenied
in Api::V1::Web::TransactionsController#create
What am I doing wrong? How can I fix it? Thanks in advance.

Multiple nested routes to the same controller. Couldn't find XYZ without an ID

I'm refactoring my application to use 1 level deep nested resources everywhere, it's a JSON-only API. Here's a trimmed version of my routes.rb:
resources :measurements, only: [:index, :show] do
resource :tag_node, controller: :physical_nodes, only: [:show]
resource :anchor_node, controller: :physical_nodes, only: [:show]
resource :experiment, only: [:show]
end
resources :physical_nodes, only: [:index, :show] do
resources :tag_nodes, controller: :measurements, only: [:index]
resources :anchor_nodes, controller: :measurements, only: [:index]
end
resources :experiments, only: [:index, :show] do
resources :measurements, only: [:index]
end
And my trimmed down models:
class Measurement < ActiveRecord::Base
self.table_name = 'measurement'
self.primary_key = 'id'
belongs_to :physical_node, foreign_key: :tag_node_id
belongs_to :physical_node, foreign_key: :anchor_node_id
belongs_to :experiment, foreign_key: :experiment_id
end
class PhysicalNode < ActiveRecord::Base
self.table_name = 'physical_node'
self.primary_key = 'id'
has_many :measurements, foreign_key: :tag_node_id
has_many :measurements, foreign_key: :anchor_node_id
end
class Experiment < ActiveRecord::Base
self.table_name = 'experiment'
self.primary_key = 'id'
has_many :measurements, foreign_key: :experiment_id
end
1.:
What works:
GET /experiments/4/measurements.json works fine
What doesn't work: (everything else ;) )
GET /measurements/2/experiment.json
Error message:
Processing by ExperimentsController#show as HTML
Parameters: {"measurement_id"=>"2"}
ActiveRecord::RecordNotFound (Couldn't find Experiment without an ID)
This should be easy to fix. More important is:
2.:
GET "/measurements/2/tag_node"
Processing by PhysicalNodesController#show as HTML
Parameters: {"measurement_id"=>"2"}
How can I get rails to call it tag_node_id instead of measurement_id?
Solution:
After a long chat with 'dmoss18', it became clear that it makes no sense to put the tag_nodes and anchor_nodes as child elements of physical_nodes, as they only exist in the measurements table.
So now my routes.rb looks like this:
resources :measurements, only: [:index, :show, :create]
resources :physical_nodes, only: [:index, :show]
resources :tag_nodes, only: [] do
resources :measurements, only: [:index]
end
resources :anchor_nodes, only: [] do
resources :measurements, only: [:index]
end
resources :experiments, only: [:index, :show] do
resources :measurements, only: [:index]
end
I've also removed all those only childs, because this is not the way the database was designed.
1: Your ExperimentsController#show action is likely looking for params[:id] to find the experiment when rails is passing in the measurement id. You will need to do something like the following:
#experiment = Experiment.find(params[:measurement_id])
However, this will not work since your experiment table doesn't have a measurement_id column. I wouldn't suggest nesting experiments as a resource of measurements. That is not how your database is laid out. If you still want to nest it though, here is what you need to do:
#experiment = Measurement.find(measurement.experiment_id).experiment
Your "has_many" and "belongs_to" don't need the "foreign_key" attribute on them. Rails will take care of this itself.
2: Since your parent resource of this route is Measurement, rails will assume the id parameter is called :measurement_id. Update your associations like this:
class Measurement < ActiveRecord::Base
self.table_name = 'measurement'
self.primary_key = 'id'
belongs_to :tag_node, :class_name => 'PhysicalNode', :foreign_key => :tag_node_id
belongs_to :anchor_node, :class_name => 'PhysicalNode', :foreign_key => :anchor_node_id
belongs_to :experiment
end
class PhysicalNode < ActiveRecord::Base
self.table_name = 'physical_node'
self.primary_key = 'id'
has_many :tag_node, :class_name => 'Measurement', :foreign_key => :tag_node_id
has_many :anchor_node, :class_name => 'Measurement', :foreign_key => :anchor_node_id
end
class Experiment < ActiveRecord::Base
self.table_name = 'experiment'
self.primary_key = 'id'
has_many :measurements
end
I would not nest anything under the measurements resource since it is a child resource, and not a parent resource. Since your /measurements/1/[anythingelse] is a measurements route, rails assumes the id is called :measurement_id. You are nesting things under the measurement resource/object whose id is 1. In other words, you are saying that measurement x HAS tag_nodes and HAS anchor_nodes, which isn't really true.
If you still wanted to, you could create individual actions in your controller for each resource, like this:
resources :measurements, only: [:index, :show] do
resource :tag_node, controller: :physical_nodes, only: [:show_tag]
resource :anchor_node, controller: :physical_nodes, only: [:show_anchor]
resource :experiment, only: [:show]
end
Create a show_tag and show_anchor action in your physical_nodes controller. These actions would then look for params[:measurement_id)

Resources