STI and use base model for forms and view - ruby-on-rails

I have a Page model in my app with all forms and views logic done.
I have a column "template" inside my Page model, dealing with only one template at the moment (article), which was a simple string until now
I want to implement a new kind of template (BasePage), but as they have a lot in common but very different logic inside controllers and models for some methods, I decided to go with STI
I created class Article < Pageand class BasePage < Page
I added self.inheritance_column = 'template'
It works well inside my console when I create a new Article or new BasePage, but my create form is now broken
The form :
= form_with model: [:admin, #page] do |form|
The error
undefined method `admin_base_pages_path' for #<ActionView::Base:0x000000000507d0>
I also tried :
= form_with url: admin_pages_path
Error :
undefined method `permit' for nil:NilClass
Is there any way to use STI but keep Page behaviour everywhere by default, except if opposite is specified?
I don't understand why my form is not working anymore and why it tries to access base_path
Also, as I first developped my app using Page model, I want to avoid changing everything to make it work, as I don't want Article and BasePage separated everywhere
Thanks a lot

Related

Rails Model method that builds a string of links

I have a method on a model called Photo. I have it finding a selection of things from elsewhere in my app. All I need it to do at the end is to create a string of links that I can then output later on when the method is called on an instance.
My code is:
cars.map { |c| link_to(c.name, c) }.join(" AND ")
But i'm hitting this error:
undefined method `link_to' for #<Photo
Any ideas how to fix this?
link_to is a view helper which means it's only available in Rails views by default because it's a router / request concern.
If you specifically want to use link_to you have to include it or reference it directly.
See this SO answer
include ActionView::Helpers::UrlHelper
...
cars.map { |c| link_to(c.name, c) }.join(" AND ")
There are other ways of getting paths than using link_to that I would recommend you consider:
It's arguable that the Rails team would tell you to use UrlFor as the tip in that link suggests:
Tip: If you need to generate URLs from your models or some other place, then ActionController::UrlFor is what you're looking for. Read on for an introduction. In general, this module should not be included on its own, as it is usually included by url_helpers (as in Rails.application.routes.url_helpers).
UrlFor also allows one to access methods that have been auto-generated from named routes.
class User < ActiveRecord::Base
include Rails.application.routes.url_helpers
def base_uri
# named_route method that uses UrlFor
user_path(self)
end
end
User.find(1).base_uri # => "/users/1"
create your own concern to bring in route helpers via ActionMailer as this article suggests
As you may see if you scroll through other SO questions about including view helpers in models, there is pushback on using router and request -based methods outside of controllers and views because it violates the principles of MVC.
I think your use case can give you some peace of mind about this, but it's worth knowing the water is murky and some people may advise you otherwise.
The traditional Rails wisdom (and what I'm about to give you here) is that models should not be creating HTML. They also shouldn't have methods that return HTML. Creating HTML <a> tags should be done much closer to the user interface: in a view template or maybe in a view helper. One reason is that the particular way the hyperlink should be generated is a concern of the view. (Does it need a nofollow attribute? class attributes? This will change, even from one view to another.) And the model should not have any knowledge of these details.
When you do generate links in the views, then you have access to all the helpers such as link_to.
Instead, as I understand it, a model should be responsible for returning its own data. Maybe in your case that'd be an array of dicts of :label, :url. I.e., pure data that'd be easy to pass to link_to.
Hope that helps!

How to create a form to trigger model method

I would like to know how to create a HTML form to send data to rails application so that specific model method guess gets triggered.
I managed to get this work in rails console, however even using HTML form guides from https://guides.rubyonrails.org/form_helpers.html I can't seem to get my head around this concept.
to generate the basic setup I used:
rails g scaffold Riddle content image_url
my model file:
models/riddle.rb
class Riddle < ApplicationRecord
def guess(guess)
content == guess
end
end
If the guess is correct(equal to content of the current object) user should see "correct guess" on the HTML page.
A form upon submit can hit a controller action. So you could do something like this:
Have a view which has the form has a form which makes a GET/POST request to an action in RiddlesController; lets call it validate_guess passing the required parameters id of the riddle and guess which is the guess that is a user input.
app/controllers/riddles_controller.rb
class RiddlesController < ApplicationController
def validate_guess
riddle = Riddle.find param[:id]
guessed_correctly = riddle.guess(params[:guess])
if guessed_correctly
render plain: "correct guess"
else
render plain: "incorrect guess"
end
end
You would also have to declare a route for this in config/routes.rb. As this works on a single Riddle object; it would be a member_action for the Riddle resource.
resources :riddles do
member do
post :validate_guess
end
end
Controller - Its the controller that renders the view and a view can only interact with the controller. Views dont directly interact with a model, and the additional controller layer does this, which provides us with a lot of structure, i.e. we can keep all data related logic in the model(though it tends to creep out of these sometimes :)). Deal with requests generation/handling at the View/Controller level and contact model to get/set info related to the business objects.
For example - your guess method is a good example. It checks whether a given value is a correct guess for a given Riddle object and returns a boolean as result(by comparing it to some attribute in the Riddle object). Going forward one could also add authentication in the controller layer, add caching or add more presentation logic. Let the model do its job of just telling the controller if the guess is correct or not for a riddle. The controller and view then deal with how to show the user that result. (e.g. Internationalize the text, style the text shown in the view or return the response in different formats like JSON/XML/HTML or maybe on a correct guess inform some other 3rd party service that the guess is correct)
Read more about the MVC pattern here and some things it enables us to achieve here

Rails render regular erb partial to active admin layout (arb)

I'm trying to add a view that LOOKS like active admin, but doesn't need to take advantage of active admin's automatic page creations (in fact I can't because I'm not using a model for these views). In my controller, I've added
render "reports/index", layout: "active_admin"
I've seen this done in several other forums. However, I'm getting this error: undefined local variable or method 'view_factory' for :Arbre::Context
which I'm assuming is because I'm not using a full active admin page object or something. Any ideas on how to make this work?
If you don't have a model then use a custom page.

Rails 4 new form defaults from params

I am using form_for in the _form.html.erb view in order to create my form for both the edit and new actions, as per a standard scaffold.
I have a model Owner which has_many pets.
I would like to put an html link on my views/owners/show.html.erb to create a new pet for said owner. This link will point to the new action of pets_controller.rb which when accessed will render the view in pets/new.html.erb
What I want to happen is for the the owner_id to be passed with the link in the url to the new action of pets_controller.rb and then be used as the default for a collection_select in pets/new.html.erb
So I have a link to create a new pet but because that link was on a specific owner page, I want the form to create a new pet to have that owner already set, so the user does not have to select from the list.
This has to be done without changing the behaviour of the edit action/view in pets.
I know that I can pass GET arguments then access them in the controller via params, then create variables in the action which are passed to the view. I can then manually check for a default and set it in the view. I do not need assistance in coding if this is the only solution.
Is there is a better way to do this? A format with which I can pass the params such that the view will just pick them up? Without manually editing my controllers and views?
While my personal inclination would be to do as you said and pass a parameter in the link helper and then access the params array in the pets view, you may find that this is the perfect opportunity to explore Nested Resources. Essentially, you could declare owners/:owner_id/pets/:pet_id route with:
resources :owners do
resources :pets
end
You could then link to this route, and reference :owner_id without having to append the query string to the URI (making somewhat cleaner for reuse).
This is likely more work for you, but also potentially more extensible (and certainly more inline with the Rails way of doing things).
REVISION
Added the following regarding link helpers to the comments, but wanted to reflect it in the answer as well.
To show a pet should be:
<%= link_to owner_pet_path( owner_variable, pet_variable) %>
To view pets' index index should be:
<%= link_to owner_pet_path( owner_variable ) %>
The answer given to this question is fantastic.
As #ConnorCMcKee suggests it would be wise to consider nesting your routes. However, if you are a beginner as myself I found that it helped my learning to simply nest my second controller into the first (i.e. nest PetsController into OwnersController) as a first step. Then afterwards I would continue with the routes.
The method would be something like:
1./ In owners/index.html.erb:
Links to PetsController index action
The key to make this work is to send the :owner_id in your link parameters. Then that Pets index action will have access to that :owner_id and know which :owner_id called it.
2./ In PetsController you would then be able to find that Owner using that id, like so:
params[:owner_id]
Then your actions can start to take advantage of knowing what Owner called them. Remember though that all your redirects inside your PetsController need to preserve params[:owner_id]. That is because once you are inside that nested structure you have to maintain it and stay inside it and always know which :owner_id you are working with.

Rails: Connect 2 models with 1 form using Devise

Been experimenting a lot with Rails lately and I'm currently working on a project. What I wish to accomplish is to have one login form for 2 set of tables, in the same db.
One called user and the other called member. I'm on the path of making it so that if you register your email with one of them, you can't register with the other. To avoid that duplication bug if you sign up for both. However what I can't seem to figure out is how to create a form_for that checks if the login data is present in either user or member, then log them in respectively.
<%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
Not sure how to tweak this bugger.
You can overwrite Devise controllers and write your own custom create function to determine which model to call to save. You can follow the documentation here: https://github.com/plataformatec/devise#configuring-controllers
Another way I can propose is that you simply use devise controllers and only change the view. Since the forms are the same, you extract your form into a partial and then overwrite devise view files to use your partial. You can follow the documentation here to overwrite devise views: https://github.com/plataformatec/devise#configuring-views
Since your user and member are quite similar, you may want to consider subclassing your member from user (or the other way) so that the validation for email is in user and member will also gain that validation. User and member can share the same table.
For your login form, you can use the same form but you need to direct it to your own custom controller which will then checks the 2 models and then return the result. Follow the steps in overwriting devise controllers as above.

Resources