Features

In this chapter, we are going to create some users to whom we can assign our tasks. Here is a list of things we need in this feature:

  • We need a new entity called "User". Every user must have a valid display name that doesn't exceed 35 characters.
  • We won't be building UI for creating or editing user details. We can deal with that later. For now, we can add a few users using Rails console.
  • Every task should have a single user as its assignee. A user can have zero or more assigned tasks.
  • We need a new dropdown in the task form showing the list of users. We should be able to select the task's assignee from this dropdown. User dropdown
  • The task details page should show the name of its assignee. Assignee in task details

Technical design

Let us break down the requirement into smaller sub-tasks and explain our way of approach in a bit more technical way:

  • We need to create a new model User. We need a single column, name. Columns like created_at and updated_at are by default part of every single table.
  • We will be validating the field name for presence and maximum length.
  • We will add a new column in our tasks table called user_id to reference the user to whom this task is assigned.
  • Our controller for the User model will have an index action that returns a list of users. This is required to show the dropdown in the tasks form.
  • We use this API to retrieve the list of users from CreateTask and EditTask components and pass it to TaskForm through props.
  • We can add a new dropdown component using the library react-select in TaskForm component. This will be filled with the list of users it will receive through its props.
  • On submitting, the selected user's id will be passed to the create/update API of tasks. That value will be stored against the user_id column.
  • We will update the show action in the tasks controller to include details of its assignee. We can use that information to display the assignee name in the ShowTask component.

Creating migration

Let's generate a migration so that we have a table named users:

1bundle exec rails generate migration CreateUser

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

The generated migration file should look like this:

1class CreateUser < ActiveRecord::Migration[6.1]
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.1]
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

Adding a User model

Let's create a User model:

1touch app/models/user.rb

As discussed earlier, we will be validating name for presence and maximum length. Copy the following code to user.rb:

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

Adding users

We need to have some users in our database in order to assign them tasks.

To create these users, launch Rails console:

1bundle exec rails console

Now let's create two users:

1User.create!(name: 'Oliver Smith')
2User.create!(name: 'Sam Smith')

Connecting User and Task

Now we have created the User model but there is no relationship between User and Task models as of now.

To identify to whom a task is assigned we need to create a new column in the 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 the tasks table:

1bundle exec rails generate migration add_user_id_to_tasks

Change the content of the migration script to this:

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

Now, we'll add a foreign key constraint to the tasks table. This will let the database know that the user_id column is related to the id column of the users table.

An error is raised when user_id is updated with a value that doesn't exist on id of users table. This makes the application fail-fast against corrupted relations between records.

Run this command to generate migration script:

1bundle exec rails g migration AddForeignKeyToTask

Add following line to migrated file:

1class AddForeignKeyToTask < ActiveRecord::Migration[6.1]
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 a hash of options. We can use this field to supply additional configurations to the constraint definition.

In this case, we can supply the name of the referencing column (specifically, user_id) as well as on_delete behavior through the 3rd parameter.

For your information, the on_delete: :cascade option makes sure that the referencing rows, which here are the rows of the task table, also get deleted when deleting the rows of the referenced table, which is the user table over here.

In our case, when deleting users, all the tasks assigned to them will also get deleted.

Note that rows of users table won't be affected when tasks are deleted. The foreign key relation is unidirectional.

Execute migration files:

1bundle exec rails db:migrate

Defining associations in 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

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  def index
3    users = User.all.as_json(only: %i[id name])
4    render status: :ok, json: { users: users }
5  end
6
7  def create
8  end
9end

Now we need to update routes.rb:

1Rails.application.routes.draw do
2  resources :tasks, except: %i[new edit], param: :slug
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

Creating users API connector

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;

Updating TaskForm component

We are going to use react-select library rather than writing the select component from the scratch:

1yarn add react-select

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

1import React from "react";
2import Button from "components/Button";
3import Input from "components/Input";
4import Select from "react-select";
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="Todo Title (Max 50 Characters Allowed)"
30        value={title}
31        onChange={e => setTitle(e.target.value.slice(0, 50))}
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 receiving users and assignedUser from props of 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. We will be invoking the users index API from here and the result will be passed to TaskForm:

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.

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.

Update the task_params function with this:

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.

Showing user names in ShowTask component

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>{" "}
40        {taskDetails?.title}
41      </h1>
42      <h2 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
43        <span className="text-gray-600">Assigned To : </span>
44        {assignedUser?.name}
45      </h2>
46      <h2 className="pb-3 mb-3 text-md leading-5 text-bb-gray-600 text-opacity-50">
47        <span>Created By : </span>
48        {taskCreator}
49      </h2>
50    </Container>
51  );
52};
53
54export default ShowTask;

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.

In this chapter, we haven't added any tests for the User model because we don't have much to test for.

Currently, the User model is at its barebones level. As we move further, we will be adding some test cases for the User model.

Now let's commit these changes:

1git add -A
2git commit -m "Added ability to assign task to a user"
⌘K
    to navigateEnterto select Escto close
    Previous
    Next