0

I'm trying to create a form for task in my app.App has two entities:

  • Project belongs_to User
  • Task belongs_to Project

But I get this error in my view when I'm trying to create this (pretty basic) form

No route matches {:action=>"index", :controller=>"tasks"} missing required keys: [:project_id]

Here is a part of my view with this form

<div class="glyphicon glyphicon-plus col-xs-1 left_plus" ></div>
        <div class="col-xs-10" >
        <%= form_for [@project, @task],url: project_tasks_path do |f| %>
          <%= f.input :body,class: 'form-control' %>
          <%= f.submit 'Add task', class: 'btn' %>
        <% end %>

And here is the project controller:

class ProjectsController < ApplicationController
  before_action :load_project, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!


  def index
    @projects = current_user.projects unless current_user.nil?
    @task = Task.find_by(params[:project_id])
  end

  def show
    @task = @project.tasks.build
  end

  def new
    @project = current_user.projects.new
  end

  def edit
  end

  def create
    @project = current_user.projects.create(project_params)

    if @project.save
      redirect_to root_path
    else
      render :new
    end
  end

  def update
    if @project.update(project_params)
      redirect_to @project
    else
      render :edit
    end
  end

  def destroy
    @project.destroy
    redirect_to projects_path
  end

  private

  def load_project
    @project = Project.find(params[:id])
  end

  def project_params
    params.require(:project).permit(:name, :user_id)
  end

end

And the tasks controller:

class TasksController < ApplicationController
  before_action :set_task, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!

  def index
    @tasks = Task.where(project_id: project_id)
  end

  def show
    project = Project.find_by(id: params[:project_id])
    @task = Task.new(project: project)
  end

  def new
    project = Project.find_by(id: params[:project_id])
    @task = Task.new(project: project)
  end


  def edit
    project = Project.find_by(id: params[:project_id])
    @task = Task.new(project: project)
  end

  def references
    respond_to do |format|
      if @task.valid?
        format.html { redirect_to root_url }
        format.json { render :show, status: :created, location: @task }
      else
        format.html { render :home_url }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

  def create
    @project = Project.find_by(id: params[:project_id])
    @task = @project.tasks.create(task_params)

    respond_to do |format|
      if @task.valid?
        format.html { redirect_to root_url }
        format.json { render :show, status: :created, location: @task }
      else
        format.html { render :home_url }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end


  def update
    respond_to do |format|
      if @task.update(task_params)
        format.html { redirect_to root_url }
        format.json { render :home_url, status: :ok, location: @task }
      else
        format.html { render :root_url }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

  def edit
  end

  def destroy
    @task.destroy
    respond_to do |format|
      format.html { redirect_to root_url }
      format.json { head :no_content }
    end
  end

  private
    def set_task
      @task = Task.find(params[:id])
    end

    def task_params
      params.require(:task).permit(:deadline, :body, :project_id)
    end
end

The routes file:

devise_for :users, :controllers => { :omniauth_callbacks => "callbacks" }
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
  root 'projects#index'

  resources :projects do
    resources :tasks
  end

And my routes:

projects#index
                   project_tasks GET      /projects/:project_id/tasks(.:format)          tasks#index
                                 POST     /projects/:project_id/tasks(.:format)          tasks#create
                new_project_task GET      /projects/:project_id/tasks/new(.:format)      tasks#new
               edit_project_task GET      /projects/:project_id/tasks/:id/edit(.:format) tasks#edit
                    project_task GET      /projects/:project_id/tasks/:id(.:format)      tasks#show
                                 PATCH    /projects/:project_id/tasks/:id(.:format)      tasks#update
                                 PUT      /projects/:project_id/tasks/:id(.:format)      tasks#update
                                 DELETE   /projects/:project_id/tasks/:id(.:format)      tasks#destroy
                        projects GET      /projects(.:format)                            projects#index
                                 POST     /projects(.:format)                            projects#create
                     new_project GET      /projects/new(.:format)                        projects#new
                    edit_project GET      /projects/:id/edit(.:format)                   projects#edit
                         project GET      /projects/:id(.:format)                        projects#show
                                 PATCH    /projects/:id(.:format)                        projects#update
                                 PUT      /projects/:id(.:format)                        projects#update
                                 DELETE   /projects/:id(.:format)                        projects#destroy

Can somebody please help me to clarify where is the problem and what is the problem?

Mikhah
  • 139
  • 12

3 Answers3

1

The problem is with your TasksController#index action. What is project_id there? For accessing a project's tasks, the project needs to exist in the first place. And not just that. To access any CRUD action on tasks, a project has to exist first.

Modify your TasksController as

class TasksController < ApplicationController
  before_action :set_project
  before_action :set_task, only: [:show, :edit, :update, :destroy]
  before_action :authenticate_user!

  def index
    @tasks = @project.tasks
  end

  def show
    #makes use of set_task
  end

  def new
    @task = @project.tasks.new
  end


  def edit
  end

  def references
    respond_to do |format|
      if @task.valid?
        format.html { redirect_to root_url }
        format.json { render :show, status: :created, location: @task }
      else
        format.html { render :home_url }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end

  def create
    @task = @project.tasks.create(task_params)
    respond_to do |format|
      if @task.valid?
        format.html { redirect_to root_url }
        format.json { render :show, status: :created, location: @task }
      else
        format.html { render :home_url }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end


  def update
    respond_to do |format|
      if @task.update(task_params)
        format.html { redirect_to root_url }
        format.json { render :home_url, status: :ok, location: @task }
      else
        format.html { render :root_url }
        format.json { render json: @task.errors, status: :unprocessable_entity }
      end
    end
  end


  def destroy
    @task.destroy
    respond_to do |format|
      format.html { redirect_to root_url }
      format.json { head :no_content }
    end
  end

  private

  def set_project
    @project = current_user.projects.find(params[:project_id]
  end

  def set_task
    @task = @project.tasks.find(params[:id])
  end

  def task_params
    params.require(:task).permit(:deadline, :body, :project_id)
  end
end

Since we have defined a before_action set_project, @project will be available to all methods. Note that set_projectfinds the project from the projects created by the current user.

In the ProjectsController#index, you won't actually get a value for params[:project_id]. Modify your index action to

def index
@projects = current_user.projects unless current_user.nil?
end

And I don't understand your show method. The build method is actually used to create a in-memory representation of an object. The show method of projects_controller can be used to display the project along with its tasks. If this is what you need, change you show action in ProjectsController to

def show
  #@project is available from load_project
  @tasks = @project.tasks
end

You could also modify your load_project project as

def load_project
  begin
    @project = Project.find(params[:id]) #raises an exception if project not found        
  rescue ActiveRecord::RecordNotFound
    redirect_to projects_path
  end
end

To know more about rescuing exceptions, see Begin, Rescue and Ensure in Ruby?

For more, see http://blog.8thcolor.com/en/2011/08/nested-resources-with-independent-views-in-ruby-on-rails/

Community
  • 1
  • 1
Arun Kumar Mohan
  • 11,517
  • 3
  • 23
  • 44
  • I've modified the controller - but the error is still the same – Mikhah Aug 08 '16 at 00:13
  • What url are trying to access? Remember you need a project id to be passed in the url. Url should be something like `http://localhost:3000/projects/3/tasks` – Arun Kumar Mohan Aug 08 '16 at 00:16
  • I'm trying to get localhost:3000/projects/36 - but still getting this error – Mikhah Aug 08 '16 at 00:20
  • Why do you need @task in projects/index.html.erb? For displaying a form in projects/index.html.erb? The issue is with `params[:project_id]`. You won't get that from the `params` hash in the `index` action – Arun Kumar Mohan Aug 08 '16 at 00:21
  • So what should I do with this situation? – Mikhah Aug 08 '16 at 00:27
  • Tell me why you need `@task` in the index action. It'll help me resolve the issue. – Arun Kumar Mohan Aug 08 '16 at 00:27
  • Well,actually this may be some shitty decision from me, but I just want to view my projects' tasks here.Should I move this variables to another action? – Mikhah Aug 08 '16 at 00:32
  • I have updated my answer. Have a look at it. I hope this resolves your issue. – Arun Kumar Mohan Aug 08 '16 at 00:36
  • Well I've made all of your updates and now the error is - undefined method `tasks' for nil:NilClass NoMethodError in ProjectsController#index – Mikhah Aug 08 '16 at 00:40
  • It just means you're searching for a non-existent project. So, `@project` is nil. And you can't call the `tasks` method on a nil object. – Arun Kumar Mohan Aug 08 '16 at 00:42
  • I have updated my answer to rescue an exception(when a project doesn't exist). I hope everything works now. – Arun Kumar Mohan Aug 08 '16 at 00:50
  • 1 minute please there is some syntax error in this exception, I'll try to figure this – Mikhah Aug 08 '16 at 00:54
  • Thats because you there's no `@projects` variable available to you from show action. And in fact, in the show page, you would only want to display a particular project. So, replace the loop with and try to display the attributes of `@project` and not `@projects`. You can also create a new loop to display all the tasks like `<% @tasks.each do |t| %>... <% end %>` – Arun Kumar Mohan Aug 08 '16 at 00:54
  • The humour is that the project#index page is the root page of the app.I'll create some another root page or set the devise page to root and I think the issue will be solved, TY – Mikhah Aug 08 '16 at 00:59
  • I have modified the code to redirect to the projects_path. You don't have to create a new root page. – Arun Kumar Mohan Aug 08 '16 at 01:01
  • And in this case I still don't have the @projects in my index or show actions) – Mikhah Aug 08 '16 at 01:05
  • Updated my code for index action of `ProjectsController`. Why do you need `@task` in index action anyway? – Arun Kumar Mohan Aug 08 '16 at 01:09
  • I don't know,as I said - maybe it was just a shitty solution) well now there is another error on the view - <%= form_for [@project, @task] do |f| %> in views/projects/index.html.erb has `First argument in form cannot contain nil or be empty` so I guess the @project wasn't loaded into this action – Mikhah Aug 08 '16 at 01:16
  • Remove the form from index action, because we don't have access to the `@project` or `@task` variable in the index action. Maybe you can move it to the show action of `ProjectsController`. Add `@task = @project.tasks.new` in show action and you can use the form template in `projects/show.html.erb` – Arun Kumar Mohan Aug 08 '16 at 01:19
  • hm, will I be able to set projects/show.html.erb as a root page? or it would be better to set up a redirect to this page? – Mikhah Aug 08 '16 at 01:27
  • I think `projects_path`(index) should be the root path. In the index page, for every project displayed, include a link to the show page. – Arun Kumar Mohan Aug 08 '16 at 01:29
0

The problem is with the url for submitting the form. If you check your rake routes you'd see that all your task routes would be nested under projects, therefore in passing the url option, you should have something like:

   <div class="glyphicon glyphicon-plus col-xs-1 left_plus" ></div>
    <div class="col-xs-10" >
    <%= form_for [@project, @task],url: project_tasks_path(@project) do |f| %>
      <%= f.input :body,class: 'form-control' %>
      <%= f.submit 'Add task', class: 'btn' %>
    <% end %>

or even better, I think you should be able to do that without passing the url option:

    <div class="glyphicon glyphicon-plus col-xs-1 left_plus" ></div>
     <div class="col-xs-10" >
     <%= form_for [@project, @task] do |f| %>
      <%= f.input :body,class: 'form-control' %>
      <%= f.submit 'Add task', class: 'btn' %>
    <% end %>

UPDATE

def index
  project = Project.find(params[:project_id])
  @projects = ProjectsViewPresenter.new(project)
end

# presenters/projects_view_presenter.rb
class ProjectsViewPresenter
  attr_reader :project
  def initialize(project)
    @project = project
  end

  def tasks
   @tasks ||= project.tasks
  end

  def task
   @task ||= tasks.new
  end
end

Your form_for would now be like this:

   <div class="glyphicon glyphicon-plus col-xs-1 left_plus" ></div>
    <div class="col-xs-10" >
    <%= form_for [@project.project, @project.task] do |f| %>
      <%= f.input :body,class: 'form-control' %>
      <%= f.submit 'Add task', class: 'btn' %>
    <% end %>
oreoluwa
  • 5,553
  • 2
  • 20
  • 27
  • I've tried to add (@project) and (project) to my url option - but now I'm getting an error undefined method to_key' for # Did you mean? to_query to_set to_ary` – – Mikhah Aug 07 '16 at 23:38
  • No, you have instantiate the @project as an instance variable, that is in your new action, you should change from `project` to `@project` or use a Facade object to manage your view objects. – oreoluwa Aug 07 '16 at 23:39
  • I've tried project_tasks_path(@project) , but now I get almost the same error `No route matches {:action=>"index", :controller=>"tasks", :project_id=>nil} missing required keys: [:project_id] ` – Mikhah Aug 07 '16 at 23:41
  • Wait, are you using your new action or index action? – oreoluwa Aug 07 '16 at 23:41
  • I'm using an index action – Mikhah Aug 07 '16 at 23:42
  • he's using index action of projects_controller to show the form.. the problem is in how @task is declared ... – davideghz Aug 07 '16 at 23:43
  • You're instantiating the `@tasks`, which would be a `ActiveRecord::Relation` object, I believe you want to use the `@tasks`, you'd need a reference to `@task` as well, which would be `@task = @tasks.new`, you could use a presenter or facade to not clutter your actions with too many instance variables – oreoluwa Aug 07 '16 at 23:46
  • Intrinsically, that means you'd need a reference to these 3 instance variables in your action: `@project, @tasks, @task` – oreoluwa Aug 07 '16 at 23:48
  • @oreoluwa I've added `@task = @tasks.new` to my tasks controller and updated the view to look like `form_for [@project, @tasks, @task]` but it still doesn't work( – Mikhah Aug 07 '16 at 23:51
  • nope, your form_for should be `form_for [@project, @task]` – oreoluwa Aug 07 '16 at 23:57
  • @oreoluwa I've also tried to reference all 3 variables in my question - and it still doesn't work – Mikhah Aug 07 '16 at 23:57
  • @oreoluwa can you please update your answer with your varian of tasks controller? – Mikhah Aug 07 '16 at 23:58
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/120375/discussion-between-mikhah-and-oreoluwa). – Mikhah Aug 08 '16 at 00:03
0

If the form to create a new task is in projects/index.html.erb you should make a @task variable available to the view; try to change the index action as follow:

def index
  @projects = current_user.projects unless current_user.nil?
  @task = Task.new
end
davideghz
  • 3,596
  • 5
  • 26
  • 50
  • I've made some changes that @oreoluwa proposed - index action of tasks controller - def index @tasks = Task.where(project_id: project_id) @task = @tasks.new @project = Project.find_by(id: params[:project_id]) end but the code still doesn't work ,now with next error - error undefined method `tasks_path' for #<#:0x007fe91c922db0> Did you mean? asset_path – Mikhah Aug 08 '16 at 00:10
  • I don't understand the downvote.. anyway, it looks like @oreoluwa is helping you in chat. Let us know if you get your code to work :) – davideghz Aug 08 '16 at 00:35
  • I didn't made those downwotes and oreoluwa is silent – Mikhah Aug 08 '16 at 00:37