I am learning and building my first app with Ruby on Rails by cloning and adjusting an existing project. I got stuck in the writing a controller and hope that someone has a tip to help me out.
Context
I am building a trainings platform: Users can design a training, these trainings can be given on several dates (I call these trainingsessions 'thrills'), (other) users can subscribe to these thrills with reservations.
Conceptualization of models
Complication
I got the users, trainings and reservations model running now but I want to add the thrills model in between (I know this is not the easiest way, but I followed a training that did not include the thrills model). I setup the model and a simple view where a trainer should be able to add thrills to an existing training (in trainings/edit). Unfortunately after a day of trying I have not managed to do so, I keep getting:
NoMethodError in ThrillsController#create
NoMethodError (undefined method `thrills' for nil:NilClass): (line 16)
My TrainingsController for def edit looks like
def edit
if current_user.id == #training.user.id
#photos = #training.photos
#thrill = #training.thrills.create(thrill_params)
#thrills = #training.thrills
else
redirect_to root_path, notice: "Je hebt hier helaas geen toegang tot"
end
end
def update
if #training.update(training_params)
if params[:images]
params[:images].each do |image|
#training.photos.create(image: image)
end
end
#thrill = #training.thrills.create(thrill_params)
#thrills = #training.thrills
#photos = #training.photos
redirect_to edit_training_path(#training), notice: "Updated..."
else
render:edit
end
end
And my ThrillsController looks like
class ThrillsController < ApplicationController
def create
#thrill = #training.thrills.create(thrill_params)
redirect_to #thrill.training, notice: "Je thrill is aangemaakt!"
end
private
def thrill_params
params.require(:thrill).permit(:trilldate, :thrillhr, :thrillmin, :training_id)
end
end
And my form to add a thrill in views/thrills/_form.html.erb which is rendered in views/trainings/edit.html.erb
<%= form_for([#training.thrills.new]) do |f| %>
<div class="form-group">
<%= f.text_field :thrillhr, placeholder: "Uur", class: "form-control" %>
</div>
<%= f.hidden_field :training_id, value: #training.id %>
<div class="actions">
<%= f.submit "Create", class: "btn btn-primary" %>
</div>
<% end %>
The full code of the app can be found here https://github.com/chrisrutte/musc
Question
I'm obviously doing something simple wrong here, so I hope someone can provide me the hint how to save new thrills in my Thrills model.
If any more information is needed, don't hestitate to ask me.
Thanks in advance!
First lets look at what the models for this kind of setup would look like:
class Training < ActiveRecord::Base
has_many :thrills
has_many :users, through: :thrills
end
class Thrills < ActiveRecord::Base
belongs_to :training
has_many :reservations
has_many :users, through: :reservations
end
class Reservation < ActiveRecord::Base
belongs_to :user
belongs_to :thrill
has_one :training, though: :thrill
end
class User < ActiveRecord::Base
has_many :reservations
has_many :thrills, through: :reservations
has_many :trainings, through: :thrills
end
Make sure you read the Rails guide on associations as this is quite complex.
Notice that Reservation works as a join model.
Reservation also has an indirect relation to Training. This is to avoid having duplicate foreign keys on several levels. The same applies to the User model. The main reason is that ActiveRecord will only write the foreign key in once place!
When setting up the routes you will most likely want to use nesting:
resources :trainings, shallow: true do
resources :thrills
end
We can then setup the controller.
class ThrillsController < ApplicationController
before_action :set_training!, only: [:new, :create, :index]
before_action :set_thrill!, only: [:show, :edit, :update, :destroy]
# GET /trainings/:training_id/thrills/new
def new
#thrill = #training.thrills.new
end
# POST /trainings/:training_id/thrills
def create
#thrill = #training.thrills.new(thrill_params)
if #thrill.save
redirect_to #thrill
else
render :new
end
end
# GET /trainings/:training_id/thrills
def index
#thrills = #training.thrills
end
# this is not nested.
# GET /thrills/:id
def show
end
# this is not nested.
# GET /thrills/:id/edit
def edit
end
# this is not nested.
# PUT|PATCH /thrills/:id
def update
if #thrill.update(thrill_params)
redirect_to #thrill
else
render :edit
end
end
# ...
private
def set_training!
#training = Training.find(params[:training_id])
end
def set_thrill!
#thill = Thrill.joins(:training).find(params[:id])
#training = #thill.training
end
def thrill_params
params.require(:thrill)
.permit(:trilldate, :thrillhr, :thrillmin, :training_id)
end
end
And lets setup the form:
<%= form_for( [#training, #thrill] ) do |f| %>
<div class="form-group">
<%= f.text_field :thrillhr, placeholder: "Uur", class: "form-control" %>
</div>
<div class="actions">
<%= f.submit "Create", class: "btn btn-primary" %>
</div>
<% end %>
Since we want to pass the training_id though the form action attribute and not in the form body we use form_for( [#training, #thrill] ) which will give us the path /trainings/6/thrills for example.
However for our edit form we want /thrills/1 instead. So lets fix that:
<%= form_for( #thrill.new_record? ? [#training, #thrill] : #thrill ) do |f| %>
# ...
<% end %>
Related
I have two models (teams and students) and when creating a team I want to be able to add a student to the team using their email. I can do this in the rails console by doing team.students << student but I am unsure how to translate that functionality in the controller and view.
Team controller:
def new
#team = Team.new
end
def add_student
#team = Team.find(params[:id])
#team.students << Student.find(params[:student_email])
end
def create
#team = Team.new(team_params)
if #team.save
redirect_to teams_path
else
render 'new'
end
end
private
def team_params
params.require(:team).permit(:name, student_attributes:[])
end
def current_team
team = Team.find(params[:id])
end
end
Team view:
<%= form_with(model: #team, local: true) do |f| %>
<%= f.label :name %>
<%= f.text_field :name, class: 'form-control' %>
<%= fields_for :student do |s|%>
<%= s.label :email%>
<%= s.text_field :email, class: 'form-control' %>
<% end %>
<%= f.submit "Create Team", class: "btn btn-primary" %>
<% end %>
Thank you for your help
You can do a lot better then just using HABTM:
class Team < ApplicationRecord
has_many :memberships
has_many :students, through: :memberships
end
class Student < ApplicationRecord
has_many :memberships
has_many :teams, through: :memberships
end
# rails g model team student:belongs team:belongs_to
class Membership < ApplicationRecord
belongs_to :student
belongs_to :team
validates_uniqueness_of :student_id, scope: :team_id
end
This also creates a many to many assocation but it gives you an actual model so you can access additional columns on the table (like for example if you want to add roles or keep track of who added the student to the team) and its actually a real entity in your buisness logic instead of just a peice of plumbing.
HABTM is actually quite useless.
To add/remove members from a team you create and destroy memberships.
resources :teams do
resources :memberships,
only: [:new, :create, :destroy]
shallow: true
end
class MembershipsController < ApplicationController
before_action :set_team, only: [:new, :index, :create]
# GET /teams/1/memberships/new
def new
#students = Student.where.not(id: #team.students)
#membership = #team.memberships.new
end
# POST /teams/1/memberships
def create
#membership = #team.memberships.new(membership_params)
if #membership.save
redirect_to #team, notice: "Student added to team"
else
#students = Student.where.not(id: #team.students)
render :new
end
end
# DELETE /memberships/1
def destroy
#membership.destroy
redirect_to #membership.team, notice: "Student removed from team"
end
private
def set_team
#team = Team.find(params[:team_id])
end
def set_membership
#membership = Membership.find(params[:id])
end
def membership_params
params.require(:membership)
.permit(:student_id)
end
end
<%= form_with(model: #membership, local: true) do |f| %>
<div class="field">
<%= f.label :student_id %>
<%= f.collection_select :student_ids, #students, :id, :email %>
</div>
<%= f.submit %>
<% end %>
As a rule of thumb if you're creating a method on your controller thats not one of the standard CRUD methods and it contains a synonym to of one of them (add, remove, etc) you're almost certainly doing it wrong and should treat it as separate RESTful resource.
I have three models: User, Publisher and Interest all with many to many relationships linked through three join models but only 2 out of 3 join models record the id's of their 2 parent models. my UsersPublisher model does not link User to Publisher.
My Interestscontroller proccesses a form (see code) through which I ask the user to provide Interest and Publisher. The latter gets processed via the fields_for method which allows you to pass Publisher attributes via the InterestsController. the UsersPublisher join model records the user_id but the publisher_id is nil.
I've tried putting #users_publishers in both the new and create methods of Publishers- and InterestsController. My latest attempt of using after_action in the InterestsController (see code) has also failed. I've also tried the after_action way in the PublishersController
Your helped is highly appreciated!
The UsersPublisher join model
class UsersPublisher < ActiveRecord::Base
belongs_to :user
belongs_to :publisher
end
InterestsController
class InterestsController < ApplicationController
before_action :find_user
after_action :upublisher, only: [:new]
def index
#interests = policy_scope(Interest)
end
def show
#interest = Interest.find(params[:id])
end
def new
#interest = Interest.new
#interest.publishers.build
authorize #interest
end
def create
#interest = Interest.new(interest_params)
#users_interests = UsersInterest.create(user: current_user, interest: #interest)
authorize #interest
if #interest.save
respond_to do |format|
format.js
format.html {redirect_to root_path}
end
flash[:notice] = 'Thank you, we will be in touch soon'
else
respond_to do |format|
format.js { render }
format.html { render :new }
end
end
end
def edit
#interest = Interest.find(params[:id])
authorize #interest
end
def update
#interest = Interest.find(params[:id])
#interest.update(interest_params)
if #interest.save
flash[:notice] = 'Your interest has been added'
else
flash[:notice] = 'Oops something went wrong'
end
end
private
def interest_params
params.require(:interest).permit(:name, publishers_attributes: [:publisher,:id, :feed])
end
def find_user
#user = current_user
end
def upublisher
#users_publishers = UsersPublisher.create(publisher: #publisher, user: current_user)
end
end
Form
<%= form_for [#user, #interest] do |f| %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= f.fields_for :publishers do |ff| %>
<%= ff.label :publisher %>
<%= ff.text_field :publisher %>
<%= ff.label :feed %>
<%= ff.text_field :feed %>
<%end%>
<%= f.submit "Submit" %>
<%end%>
Since you're using fields_for, you'll want to make sure you have accepts_nested_attributes_for:
class UsersPublisher < ActiveRecord::Base
belongs_to :user
belongs_to :publisher
accepts_nested_attributes_for :publisher
end
This should fix your issue (if it's as you outlined).
Your question is pretty broad, so I don't know whether the above will work. Below are my notes...
From the looks of it, your structure is very complicated; you should work to make it as simple as possible. In the case of creating "interests", you may wish to get rid of the form completely:
#config/routes.rb
resources :publishers do
resources :interests, path: "interest", only: [:create, :destroy] #-> url.com/publishers/:publisher_id/interest
end
#app/controllers/interests_controller.rb
class InterestsController < ApplicationController
before_action :set_publisher
def create
current_user.interests.create publisher: #publisher
end
def destroy
#interest = current_user.interests.find_by publisher_id: #publisher.id
current_user.interests.delete #interest
end
private
def set_publisher
#publisher = UserPublisher.find params[:publisher_id]
end
end
You'd be able to use the above as follows:
<%= link_to "Add Interest", publisher_interest_path(#publisher), method: :post %>
<%= link_to "Remove Interest", publisher_interest_path(#publisher), method: :delete %>
Thinking about it properly, you've got a pretty bad structure.
I'd do something like this:
#app/models/user.rb
class User < ActiveRecord::Base
has_many :interests
has_many :publishers, through: :interests
end
#app/models/interest.rb
class Interest < ActiveRecord::Base
belongs_to :user
belongs_to :publisher
accepts_nested_attributes_for :publisher
end
#app/models/publisher.rb
class Publisher < ActiveRecord::Base
has_many :interests,
has_many :users, through: :interests
end
This should give you the ability to create interests for any number of users and publishers. If you create a publisher for a specific user, you can use accepts_nested_attributes_for to pass the appropriate data:
#config/routes.rb
resources :users do
resources :interest, only: [:new, :create, :destroy] #-> url.com/users/:user_id/interests/new
end
#app/controllers/interests_controller.rb
class InterestsController < ApplicationController
def new
#user = User.find params[:user_id]
#interest = #user.interests.new
#interest.publisher.build
end
def create
#user = User.find params[:user_id]
#interest = #user.interests.new interest_params
end
private
def interest_params
params.require(:interest).permit(:user, :publisher)
end
end
#app/views/interests/new.html.erb
<%= form_for [#user, #interest] do |f| %>
<%= f.fields_for :publisher do |p| %>
<%= p.text_field :name %>
<% end %>
<%= f.submit %>
<% end %>
I have the following schema:
attribute
----------
id
name
profile
----------
user_id
attribute_id
value
user
----------
id
name
What I am trying to do is display all the attributes and then for any attribute that the user does have in the profile it fills it in and an update can be performed. My background isn't ruby but I'm testing this framework for a proof of concept before possibly migrating an app.
I have mapped the association like that on the guides on rubyonrails.org (the appointments example)
class Attribute < ActiveRecord::Base
has_many :profiles
has_many :users, through: :profiles
end
class Profile < ActiveRecord::Base
belongs_to :attribute
belongs_to :user
end
class User < ActiveRecord::Base
has_many :profiles
has_many :attributes, through: :profiles
accepts_nested_attributes_for :profiles
end
I took the approach of using nested forms but unable to pass the model through even though the accepts_nested_attributes_for has been set.
<%= form_for(#user) do |f| %>
<% Attribute.all.each do |attribute| %>
<div>
<%= attribute.name %>
</div>
<div>
<%= f.fields_for :profiles do |upp| %>
<%= upp.hidden_field :user_id, :value => #user.id %>
<%= upp.hidden_field :attribute_id, :value => #attribute.id %>
<%= upp.text_field :value %>
<% end %>
</div>
<% end %>
<div>
<%= f.submit %>
</div>
<% end %>
e.g
Attributes
A
B
C
User has attribute A
A ["hello world"]
B [ ]
C [ ]
Attributes are dynamic, so if 2 new attributes are added, D and E would be shown as well
A ["hello world"]
B [ ]
C [ ]
D [ ]
E [ ]
How can I setup this form correctly so that it can be passed through as a model for saving? I assume the json would be something like
profile_attributes { [user_id:1 attribute_id:1 value:'hello world'], [user_id:1 attribute_id:2 value:''] }
I know the above form setup is not quite right but that is just one of several attempts I tried to see what it renders.
I tried:
<%= f.fields_for #user.profiles do |pp| %>
<% end %>
I even tried manually setting the field as arrays (similiar to asp.net mvc):
id=user[profiles][user_id]
id=user[profiles][attribute_id]
Controllers (they're empty shells at the moment, I just wanted to see the json output in the console)
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#user = User.find(params[:id])
end
end
class ProfileController < ApplicationController
def edit
#user = User.find(params[:id])
end
def update
respond_to do |format|
#user = User.find(params[:id])
end
private
def user_params
params.require(:user).permit(profiles_attributes: [:user_id, :attribute_id, :value])
end
end
end
I've tried many different approaches but with no success. Sometimes the form shows duplicated fields with same values, sometimes the form is shown but value is blank and upon submission complains about unpermitted parameter yet it was set in the controller.
Of course all the above can be done using javascript but I wanted to see if it was possible just using the model approach and nesting it.
You can do like this :
move form to your users/edit.html.erb and modify your users_controller as below
class UsersController < ApplicationController
before_action :set_user, only: [:show, :edit, :update, :destroy]
def edit
#user.profiles.build
end
def update
if #user.update(user_params)
#redirect user where you want
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_user
#user = User.find(params[:id])
end
def user_params
#permit user attributes along with profile attributes
end
end
I'm currently trying to build a really simple nested form app in my quest to learn rails. In this app I have three models legal_form answer and question. I have my my answers.html.erb, set up as follows:
<%= form_for (#legal_form) do |f| %>
<h1>Title <%= #legal_form.title %></h1>
<p><%= #legal_form.description %></p>
<ul>
<% #legal_form.questions.each do |question| %>
<%= fields_for question.answers.build do |q| %>
<li>
<%= question.question_content %>
<%= q.text_field :answer_content %>
</li>
<% end =%>
<% end %>
</ul>
<p><%= f.submit "Submit" %></p>
<% end %>
Which currently grabs the three questions I have stored and renders text input boxes next to them; works without a problem. However, when I submit the values, I get the "param is missing or empty: legal_form".
I figure that this is most likely due to my strong params configuration in the legal_forms controller, see below.
class LegalFormsController < ApplicationController
before_action :find_legal_form, only: [:show, :edit, :update, :destroy, :answers]
def index
#legal_form = LegalForm.all.order("created_at DESC")
end
def show
end
def new
#legal_form=LegalForm.new
end
def create
#legal_form = LegalForm.new(legal_form_params)
if #legal_form.save
redirect_to #legal_form, notice: "Successfully created new legal form."
else
render 'new'
end
end
def edit
end
def update
if #legal_form.update(legal_form_params)
redirect_to #legal_form
else
render 'edit'
end
end
def destroy
#legal_form.destroy
redirect_to root_path, notice: "Successfully deleted form"
end
def answers
#questions=#legal_form.questions
#legal_form=LegalForm.find(params[:id])
end
private
def legal_form_params
params.reqire(:legal_form).permit(:title, :description, :questions_attribute => [:id, :question_number, :question_content, :_destroy, :answer_attributes => [:id, :answer_content, :question_id, :user_id]])
end
def find_legal_form
#legal_form=LegalForm.find(params[:id])
end
end
And, in case it's helpful, here are the models for each.
class Answer < ActiveRecord::Base
belongs_to :question
end
class LegalForm < ActiveRecord::Base
has_many :questions, :dependent => :destroy
has_many :answers, through: :entity_roles
accepts_nested_attributes_for :questions,
reject_if: proc { |attributes| attributes['question_content'].blank? },
allow_destroy: true
end
class Question < ActiveRecord::Base
belongs_to :legal_form
has_many :answers
accepts_nested_attributes_for :answers,
reject_if: proc { |attributes| attributes['question_content'].blank? },
allow_destroy: true
end
Also, as requested here's my routes file:
Rails.application.routes.draw do
resources :legal_forms do
member do
get 'answers'
end
end
resources :answers
root "legal_forms#index"
end
Any help to finally conquer nested forms would be greatly appreciated. I've been banging my head against it off and on for about a week now. Many thanks in advance.
Try in the controller
def legal_form_params
params.require(:legal_form).permit(...)
end
Also question.answers.build add it to the method of your controller and call the object that returns the responses to fields_for
UPDATE
To be sent through this form your results, should probably be like this
form
<%= f.fields_for :answers do |q| %>
...
<% end =%>
in the new method
def new
#legal_form=LegalForm.new
#answers = #legal_form.question.answers.build
end
def legal_form_params
params.require(:legal_form).permit! #temporarily
end
not tried it, but imagine how it works something like this
I am trying to get a better handle on Rail's nested resources, and made a test app with models School and Class. In my routes.rb file, I have:
resources :schools do
resources :classes
end
With the following models relationship for School and Class:
class School < ActiveRecord::Base
attr_accessible: name
has_many :classes
end
and
class Class < ActiveRecord::Base
attr_accessible: name, school_id
belongs_to :school
end
I am having difficulty getting the school_id associated with the posts created under an URL like /schools/1/posts/new. More precisely, I would like to define a helper method like current_school that could take the first half of the URI where it contains the school_id, to allow me to write functions in the controller like current_school.posts.all that will automatically pull all the posts associated with school_id = what is in the URL. Thanks!
*Edit
Here is what I have in ClassController:
class ClassesController < ApplicationController
def index
#classes = current_school.classes.all
end
def new
#class = current_school.classes.build
end
def create
#class = current_school.classes.build(params[:post])
if #class.save
redirect_to root_path #this will be further modified once I figure out what to do
else
redirect_to 'new'
end
end
private
def current_school
#current_school ||= School.find(params[:school_id])
end
end
And in the new.html.erb file:
<div class="span6 offset3">
<%= form_for([#school, #class]) do |f| %>
<%= f.label :name, "class title" %>
<%= f.text_field :name %>
<%= f.submit "Create class", class: "btn btn-large btn-primary" %>
<% end %>
</div>
When you nest your resources, you get several helper methods for free as explained here. The method you're looking for would be written as one of:
new_school_class_path(#school)
new_school_class_path(#school_id)
While your classes index page would be:
school_classes_path(#school)
school_classes_path(#school_id)
In your ClassesController, you would do something like:
def index
#classes = current_school.classes
end
def new
#class = current_school.classes.build
end
private
def current_school
#current_school ||= School.find(params[:school_id])
end