A task need to be assigned to a user to get it done. In this chapter we will see how to assign a task to a user.

Adding a User model

Let's create a User model.

1touch app/models/user.rb

Add the following lines into the user.rb file and save it.

1class User < ApplicationRecord
2end

Creating migration

Now let's generate a migration so that we have a table named users.

1bundle exec rails generate migration CreateUser

And it will create the following file:

1Running via Spring preloader in process 30090
2      invoke  active_record
3      create    db/migrate/20190209145206_create_user.rb

Open the migration file.

1class CreateUser < ActiveRecord::Migration[6.0]
2  def change
3    create_table :users do |t|
4    end
5  end
6end

Let's add name field with type string into the migration file.

1class CreateUser < ActiveRecord::Migration[6.0]
2  def change
3    create_table :users do |t|
4      t.string :name, null: false
5      t.timestamps
6    end
7  end
8end
9

Execute migration files.

1bundle exec rails db:migrate

Let's add the presence and maximum length validations to name field.

1class User < ApplicationRecord
2  validates :name, presence: true, length: { maximum: 35 }
3end

Now we have created the User model but there is no relationship between User and Task models. Theoretically, a user can have many tasks.

In order to identify to whom a task is assigned we need to create a new column in tasks table. We will call this column user_id since it will store user id of the person to whom the task is assigned.

Let's create a migration to add column user_id to tasks table.

1bundle exec rails generate migration add_user_id_to_tasks

The migration file will look like this.

1class AddUserIdToTasks < ActiveRecord::Migration[6.0]
2  def change
3    add_column :tasks, :user_id, :integer
4  end
5end

Execute the migration file.

1bundle exec rails db:migrate

Add Foreign key

Now, we'll add a foreign key constraint to the tasks table so that it has only valid user ids in user_id column.

1bundle exec rails g migration AddForeignKeyToTask

Add following line to migrated file.

1class AddForeignKeyToTask < ActiveRecord::Migration[6.0]
2  def change
3    add_foreign_key :tasks, :users, column: :user_id, on_delete: :cascade
4  end
5end

The syntax for add_foreign_key is as following.

add_foreign_key(from_table, to_table, options = {}) where from_table is the table with the key column, to_table contains the referenced primary key. The third parameter is an option which allows us to define our custom name for the column. The fourth parameter options as on_delete: :cascade. This option makes sure that child objects also get deleted when deleting parent.

Execute migration files.

1bundle exec rails db:migrate

Defining associations between Task and User models

A user can have many tasks assigned to him/her and tasks belong to a user. Rails provides us an easy way to define such associations.

Add the following line into the user.rb file.

1class User < ApplicationRecord
2  has_many :tasks, dependent: :destroy
3end

The dependent: :destroy callback would destroy all the tasks assigned to a user, if that user is destroyed.

Add the following line into the task.rb file.

1class Task < ApplicationRecord
2  validates :title, presence: true, length: { maximum: 50 }
3  belongs_to :user
4end

Adding tests for User model

Let's add a validation test for presence of name when creating a user:

1def test_user_should_be_not_be_valid_without_name
2  @user.name = ''
3  assert_not @user.valid?
4  assert_equal ["Name can't be blank"], @user.errors.full_messages
5end

We can also write a test to validate length of the value assigned to name field:

1def test_name_should_be_of_valid_length
2  @user.name = 'a' * 50
3  assert @user.invalid?
4end

Now let's validate whether the instances are belonging to correct model or not:

1def test_instance_of_user
2  assert_instance_of User, @user
3end
4
5def test_not_instance_of_user
6  task = Task.new
7  assert_not_instance_of User, task
8end

Creating index action for user

Let's create an new file app/controllers/users_controller.rb and add the following lines of code to it:

1class UsersController < ApplicationController
2  # before_action :authenticate_user_using_x_auth_token, only: [:index]
3
4  def index
5    users = User.all.as_json(only: %i[id name])
6    render status: :ok, json: { users: users }
7  end
8
9  def create
10  end
11
12  private
13
14  def user_params
15    params.require(:user).permit(:name, :email, :password, :password_confirmation)
16  end
17end

Now we need to update routes.rb

1Rails.application.routes.draw do
2  resources :tasks, except: %i[new edit]
3  resources :users, only: %i[create index]
4  resource :sessions, only: %i[create destroy]
5
6  root 'home#index'
7  get '*path', to: 'home#index', via: :all
8end

Defining list user API

Let's create a new file to define all the APIs related to user model

1touch app/javascript/src/apis/users.js

Now let's add the following code to it :

1import axios from "axios";
2
3const list = () => axios.get("/users");
4
5const usersApi = {
6  list,
7};
8
9export default usersApi;

Here, we are using the GET method to fetch all users so that we can pre-populate the Select component that we will be using in TaskForm.jsx. The Select component will help us assign the task to a particular user.

Updating TaskForm component

So we will be making use of the react-select library rather than writing the select component from scratch. This is a general point to keep in mind. Most of the reusable components that we had created, are already directly available and well maintained in the neeto-ui component library, which is BigBinary's own component library. The neeto-ui select component is composed of react-select. We are not going to make use of neeto-ui library in this application, since we can try our best to learn to create such reusable components. But in general, no need to reinvent the wheel. Thus let's keep it simple and just use the react-select library for now. Let's install it first:

1yarn add react-select

Next, replace the whole content of the TaskForm component with the following lines:

1import React from "react";
2import Select from "react-select";
3import Button from "components/Button";
4import Input from "components/Input";
5
6const TaskForm = ({
7  type = "create",
8  title,
9  setTitle,
10  assignedUser,
11  users,
12  setUserId,
13  loading,
14  handleSubmit,
15}) => {
16  const userOptions = users.map(user => ({
17    value: user.id,
18    label: user.name,
19  }));
20  const defaultOption = {
21    value: assignedUser?.id,
22    label: assignedUser?.name,
23  };
24
25  return (
26    <form className="max-w-lg mx-auto" onSubmit={handleSubmit}>
27      <Input
28        label="Title"
29        placeholder="Docs Revamp"
30        value={title}
31        onChange={e => setTitle(e.target.value)}
32      />
33      <div className="flex flex-row items-center justify-start mt-3">
34        <p className="w-3/12 leading-5 text-gray-800 text-md">Assigned To: </p>
35        <div className="w-full">
36          <Select
37            options={userOptions}
38            defaultValue={defaultOption}
39            onChange={e => setUserId(e.value)}
40            isSearchable
41          />
42        </div>
43      </div>
44      <Button
45        type="submit"
46        buttonText={type === "create" ? "Create Task" : "Update Task"}
47        loading={loading}
48      />
49    </form>
50  );
51};
52
53export default TaskForm;

Here, we are passing users and assignedUser as props to TaskForm.jsx which will be used to populate the Select component with usernames and also as a default value. Note that, when we select an item from the usernames, the corresponding id of the user is what gets passed into setUserId. If you notice the first two lines in the TaskForm component, there we are formatting out the users and assignedUser, to the format required by react-select.

Updating CreateTask component

Now while creating a task we will assign a user to that task.

Let's update app/javascript/src/components/Tasks/CreateTask.jsx and invoke the TaskForm component, which in turn would add a Select component with a list of all the users from the database. From this list, we can select an user to whom we need to assign the task. By default the task is assigned to the first user available in users array.

1import React, { useState, useEffect } from "react";
2import Container from "components/Container";
3import TaskForm from "./Form/TaskForm";
4import PageLoader from "components/PageLoader";
5import tasksApi from "apis/tasks";
6import usersApi from "apis/users";
7
8const CreateTask = ({ history }) => {
9  const [title, setTitle] = useState("");
10  const [userId, setUserId] = useState("");
11  const [users, setUsers] = useState([]);
12  const [loading, setLoading] = useState(false);
13  const [pageLoading, setPageLoading] = useState(true);
14
15  const handleSubmit = async event => {
16    event.preventDefault();
17    try {
18      await tasksApi.create({ task: { title, user_id: userId } });
19      setLoading(false);
20      history.push("/dashboard");
21    } catch (error) {
22      logger.error(error);
23      setLoading(false);
24    }
25  };
26
27  const fetchUserDetails = async () => {
28    try {
29      const response = await usersApi.list();
30      setUsers(response.data.users);
31      setUserId(response.data.users[0].id);
32      setPageLoading(false);
33    } catch (error) {
34      logger.error(error);
35      setPageLoading(false);
36    }
37  };
38
39  useEffect(() => {
40    fetchUserDetails();
41  }, []);
42
43  if (pageLoading) {
44    return <PageLoader />;
45  }
46
47  return (
48    <Container>
49      <TaskForm
50        setTitle={setTitle}
51        setUserId={setUserId}
52        assignedUser={users[0]}
53        loading={loading}
54        handleSubmit={handleSubmit}
55        users={users}
56      />
57    </Container>
58  );
59};
60
61export default CreateTask;

After submitting the form we'll get task as params with attribute user_id.

The options we get in the Select menu are populated from the response of user list API, that we pass as props. Then we are assigning id to value attribute in option and we'll display name corresponding to that id.

Start Rails sever and visit http://localhost:3000. Clicking on the create task button will redirect you to the page to create new task. Select a user from the dropdown menu, add a title, and create the task. That's it.

If we look into the SQL statements generated on the server we see that user_id is not being used in the sql statements. That's because we are not marking user_id as a safe parameter. We need to change task_params to whitelist user_id attribute.

1class TasksController < ApplicationController
2  def task_params
3    params.require(:task).permit(:title, :user_id)
4  end
5end

Now if we create a new Task record then user_id is getting stored.

Updating EditTask component

Open app/javascript/src/components/Tasks/EditTask.jsx and replace the entire content in it :

1import React, { useState, useEffect } from "react";
2import tasksApi from "apis/tasks";
3import usersApi from "apis/users";
4import Container from "components/Container";
5import PageLoader from "components/PageLoader";
6import { useParams } from "react-router-dom";
7
8import TaskForm from "./Form/TaskForm";
9
10const EditTask = ({ history }) => {
11  const [title, setTitle] = useState("");
12  const [userId, setUserId] = useState("");
13  const [assignedUser, setAssignedUser] = useState("");
14  const [users, setUsers] = useState([]);
15  const [loading, setLoading] = useState(false);
16  const [pageLoading, setPageLoading] = useState(true);
17  const { slug } = useParams();
18
19  const handleSubmit = async event => {
20    event.preventDefault();
21    try {
22      await tasksApi.update({
23        slug,
24        payload: { task: { title, user_id: userId } },
25      });
26      setLoading(false);
27      history.push("/dashboard");
28    } catch (error) {
29      setLoading(false);
30      logger.error(error);
31    }
32  };
33
34  const fetchUserDetails = async () => {
35    try {
36      const response = await usersApi.list();
37      setUsers(response.data.users);
38    } catch (error) {
39      logger.error(error);
40    } finally {
41      setPageLoading(false);
42    }
43  };
44
45  const fetchTaskDetails = async () => {
46    try {
47      const response = await tasksApi.show(slug);
48      setTitle(response.data.task.title);
49      setAssignedUser(response.data.assigned_user);
50      setUserId(response.data.assigned_user.id);
51    } catch (error) {
52      logger.error(error);
53    }
54  };
55
56  const loadData = async () => {
57    await fetchTaskDetails();
58    await fetchUserDetails();
59  };
60
61  useEffect(() => {
62    loadData();
63  }, []);
64
65  if (pageLoading) {
66    return (
67      <div className="w-screen h-screen">
68        <PageLoader />
69      </div>
70    );
71  }
72
73  return (
74    <Container>
75      <TaskForm
76        type="update"
77        title={title}
78        users={users}
79        assignedUser={assignedUser}
80        setTitle={setTitle}
81        setUserId={setUserId}
82        loading={loading}
83        handleSubmit={handleSubmit}
84      />
85    </Container>
86  );
87};
88
89export default EditTask;

When we click on the edit button on the task listing page we are redirected to the edit task page.

Updating show task component to display the assigned user and creator of the task

Now we will display the user that is assigned the task and creator of the task on task show page.

To do so, open ShowTask.jsx and add the following lines:

1import React, { useState, useEffect } from "react";
2import { useParams } from 'react-router-dom';
3
4import Container from "components/Container";
5import PageLoader from "components/PageLoader";
6import tasksApi from "apis/tasks";
7
8const ShowTask = () => {
9  const { slug } = useParams();
10  const [taskDetails, setTaskDetails] = useState([]);
11  const [assignedUser, setAssignedUser] = useState([]);
12  const [pageLoader, setPageLoader] = useState(true);
13  const [taskCreator, setTaskCreator] = useState("");
14
15  const fetchTaskDetails = async () => {
16    try {
17      const response = await tasksApi.show(slug);
18      setTaskDetails(response.data.task);
19      setAssignedUser(response.data.assigned_user);
20      setTaskCreator(response.data.task_creator);
21    } catch (error) {
22      logger.error(error);
23    }finally{
24      setPageLoader(false);
25    }
26  };
27
28  useEffect(() => {
29    fetchTaskDetails();
30  }, []);
31
32  if (pageLoader) {
33    return <PageLoader />;
34  }
35
36  return (
37    <Container>
38      <h1 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
39        <span className="text-gray-600">Task Title : </span> {taskDetails?.title}
40      </h1>
41      <h2 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
42        <span className="text-gray-600">Assigned To : </span>{assignedUser?.name}
43      </h2>
44      <h2 className="pb-3 mb-3 text-md leading-5 text-bb-gray-600 text-opacity-50">
45        <span>Created By : </span>
46        {taskCreator}
47      </h2>
48    </Container>
49  );
50};
51
52export default ShowTask;

And also, modify our Tasks#show to access the task_creator:

1def show
2  task_creator = User.find(@task.creator_id).name
3  render status: :ok, json: { task: @task,
4                              assigned_user: @task.user,
5                              task_creator: task_creator }
6end

Now, while clicking on the show button of a task in ListTasks component which is rendered in Dashboard, we will be routed to the ShowTask component. There we can see the task details, which would include the title, as well as the assigned user's name and task creator's name

Now let's commit these changes:

1git add -A
2git commit -m "Added ability to assign task to a user"