Search
⌘K
    to navigateEnterto select Escto close

    User model

    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

    • Each task on the dashboard should contain the task assignee's name along with the task's title.

    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 assigned_user_id to reference the user to whom this task is assigned.

    • We will declare a has_many association for assigned_tasks in the User model and a belongs_to association for assigned_user in Task model.

    • 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 assigned_user_id column.

    • We will update the show action in tasks controller to include details of task assignee. We can use that information to display the name of assignee in the ShowTask component.

    • We will update the TableRow component to display the task assignee's name.

    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. Let us first declare a constant for the maximum length and then we can use that constant in the length validation.

    Add the following line to config/initializers/constants.rb file:

    1module Constants
    2  is_sqlite_db = ActiveRecord::Base.connection_db_config.configuration_hash[:adapter] == "sqlite3"
    3  DB_REGEX_OPERATOR = is_sqlite_db ? "REGEXP" : "~*"
    4  MAX_TASK_TITLE_LENGTH = 125
    5  MAX_NAME_LENGTH = 255
    6end

    Note that, the value for maximum length in this case is in accordance with the W3C standards for maximum length of a name. As a matter of fact, all values for maximum lengths used in the model validations in this book are in line with the standards laid down by W3C.

    Now, update the following line within the user.rb file:

    1class User < ApplicationRecord
    2  validates :name, presence: true, length: { maximum: Constants::MAX_NAME_LENGTH }
    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 assigned_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 assigned_user_id to the tasks table:

    1bundle exec rails generate migration AddAssignedUserIdToTasks assigned_user_id:integer

    Change the content of the migration script to this:

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

    The code for the change method will be auto-populated when we run the above command. Reason being, the name of our migration is AddAssignedUserIdToTask, which is of the form add_column_name_to_table_name column_name:type.

    Rails will infer the column name and table name from the migration name itself and generate the code for that, even respecting that types passed in.

    Cool right? That's one among the many Rails magic that you will be seeing throughout this book!

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

    Adding a foreign key constraint will also ensure that an error is raised when assigned_user_id is updated with a value that is not present in the id column of the 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: :assigned_user_id
    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).

    Execute migration files:

    1bundle exec rails db:migrate

    Defining associations in models

    A user can have many tasks assigned to them and from Rails perspective, those tasks belong to a user. Rails provides us an easy way to define such associations.

    Since a user can have many tasks assigned to them, we will declare a has_many association called assigned_tasks in the User model.

    No need to worry about assigned_tasks now. It's a custom association name. We will discuss about it after adding the code to the respective file.

    Add the following line into the user.rb file:

    1class User < ApplicationRecord
    2  has_many :assigned_tasks, foreign_key: :assigned_user_id, class_name: "Task"
    3
    4  validates :name, presence: true, length: { maximum: 35 }
    5end

    By default Rails infers the associated class's name from the association name itself and the foreign key to be a column in the associated table with the same name as that of the declaring class in lower-case and _id suffixed. Let's understand this with the help of a simple example shown below:

    1class User
    2  has_many :tasks
    3end

    In the above example, tasks is the association name. Thus the associated class will be task and the foreign_key column inside tasks table will be user_id which is same as the declaring class name in lowercase with _id suffixed.

    In our case, however, we have declared an association by the name of assigned_tasks and the associated class's name cannot be inferred from the association name. This is why we have passed options specifying the foreign key name as well the class name to build an association.

    Note that, it is suggested to name associations after the class name for simplicity but we have deferred from that here because later in this book we will be adding another association between the User model and the Task model for task owner. Hence, we have named the association accordingly to avoid confusion, given that later there will be two foreign keys between Task and User models.

    Similarly we can declare a belongs_to association in the Task model. Add the following line into the task.rb file:

    1class Task < ApplicationRecord
    2  belongs_to :assigned_user, foreign_key: "assigned_user_id", class_name: "User"
    3
    4  # previous code
    5
    6  private
    7
    8    # previous code
    9end

    Don't get confused by the plural and singular association names. In Rails we follow certain naming conventions. To understand the naming convention we have used for associations you can refer to naming model associations section in the in-depth chapter where we have summarized most of the Rails naming conventions. You can refer to it whenever you are confused about the naming and come back.

    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
    6end

    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: :index
    4
    5  root 'home#index'
    6  get '*path', to: 'home#index', via: :all
    7end

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

    Updating TasksController

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

    We also need to update the show action of TasksController to respond with the task assignee along with other task details.

    If you recall we had added a belongs_to association in the Task model called assigned_user. When we declare an association inside a model, Rails creates a method inside the model by the name of the association which returns the associated object when invoked.

    In this case, the method name will be assigned_user and it will return the task assignee when invoked. We can use the assigned_user method to obtain the task assignee inside the show action of TasksController.

    If you are keen to know how this works under the hood, you can refer to the Rails macros and metaprogramming chapter in this book which deals with this topic in depth.

    To introduce the required changes, update the TasksController with the following lines of code:

    1class TasksController < ApplicationController
    2  before_action :load_task, only: %i[show update destroy]
    3
    4  def index
    5    tasks = Task.all
    6    render status: :ok, json: { tasks: tasks }
    7  end
    8
    9  def create
    10    task = Task.new(task_params)
    11    if task.save
    12      render status: :ok, json: { notice: 'Task was successfully created' }
    13    else
    14      errors = task.errors.full_messages.to_sentence
    15      render status: :unprocessable_entity, json: { error: errors  }
    16    end
    17  end
    18
    19  def show
    20    render status: :ok, json: { task: @task, assigned_user: @task.assigned_user }
    21  end
    22
    23  def update
    24    if @task.update(task_params)
    25      render status: :ok, json: { notice: 'Successfully updated task.' }
    26    else
    27      render status: :unprocessable_entity,
    28        json: { error: @task.errors.full_messages.to_sentence }
    29    end
    30  end
    31
    32  def destroy
    33    if @task.destroy
    34      render status: :ok, json: { notice: 'Successfully deleted task.' }
    35    else
    36      render status: :unprocessable_entity,
    37        json: { error: @task.errors.full_messages.to_sentence }
    38    end
    39  end
    40
    41  private
    42
    43    def task_params
    44      params.require(:task).permit(:title, :assigned_user_id)
    45    end
    46
    47    def load_task
    48      @task = Task.find_by(slug: params[:slug])
    49      unless @task
    50        render status: :not_found, json: { error: t('task.not_found') }
    51      end
    52    end
    53end

    After making the changes we just discussed, assigned_user_id will get stored while creating a new task and we will get assigned_user along with other task details in the frontend upon fetching a task.

    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.

    Updating EditTask component

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

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

    Fully replace ShowTask.jsx with the following lines of code:

    1import React, { useState, useEffect } from "react";
    2import { useParams, useHistory } 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 [pageLoading, setPageLoading] = useState(true);
    13
    14  const history = useHistory();
    15
    16  const updateTask = () => {
    17    history.push(`/tasks/${taskDetails.slug}/edit`);
    18  };
    19
    20  const fetchTaskDetails = async () => {
    21    try {
    22      const response = await tasksApi.show(slug);
    23      setTaskDetails(response.data.task);
    24      setAssignedUser(response.data.assigned_user);
    25    } catch (error) {
    26      logger.error(error);
    27    } finally {
    28      setPageLoading(false);
    29    }
    30  };
    31
    32  useEffect(() => {
    33    fetchTaskDetails();
    34  }, []);
    35
    36  if (pageLoading) {
    37    return <PageLoader />;
    38  }
    39
    40  return (
    41    <Container>
    42      <h1 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">Task Title : </span>{" "}
    44        {taskDetails?.title}
    45      </h1>
    46      <div className="bg-bb-env px-2 mt-2 mb-4 rounded">
    47        <i
    48          className="text-2xl text-center transition cursor-pointer duration-300ease-in-out ri-edit-line hover:text-bb-yellow"
    49          onClick={updateTask}
    50        ></i>
    51      </div>
    52      <h2 className="pb-3 pl-3 mt-3 mb-3 text-lg leading-5 text-gray-800 border-b border-gray-500">
    53        <span className="text-gray-600">Assigned To : </span>
    54        {assignedUser?.name}
    55      </h2>
    56    </Container>
    57  );
    58};
    59
    60export default ShowTask;

    Now, while clicking on the show button of a task in the Table 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.

    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.

    Showing task assignee in Dashboard

    So far we have only been showing the task title in each of the tasks listed on the dashboard. To display task assignee's name along with the task title in dashboard update the TableRow component like so:

    1import React from "react";
    2import PropTypes from "prop-types";
    3
    4const TableRow = ({ data, destroyTask, showTask }) => {
    5  return (
    6    <tbody className="bg-white divide-y divide-gray-200">
    7      {data.map(rowData => (
    8        <tr key={rowData.id}>
    9          <td
    10            className="block w-64 px-6 py-4 text-sm font-medium
    11            leading-8 text-bb-purple capitalize truncate"
    12          >
    13            {rowData.title}
    14          </td>
    15          <td
    16            className="px-6 py-4 text-sm font-medium
    17            leading-5 text-gray-900 whitespace-no-wrap"
    18          >
    19            {rowData.assigned_user.name}
    20          </td>
    21          <td className="px-6 py-4 text-sm font-medium leading-5 text-right cursor-pointer">
    22            <a
    23              className="text-bb-purple"
    24              onClick={() => showTask(rowData.slug)}
    25            >
    26              Show
    27            </a>
    28          </td>
    29          <td
    30            className="px-6 py-4 text-sm font-medium
    31            leading-5 text-right cursor-pointer"
    32          >
    33            <a
    34              className="text-red-500
    35              hover:text-red-700"
    36              onClick={() => destroyTask(rowData.slug)}
    37            >
    38              Delete
    39            </a>
    40          </td>
    41        </tr>
    42      ))}
    43    </tbody>
    44  );
    45};
    46
    47TableRow.propTypes = {
    48  data: PropTypes.array.isRequired,
    49  destroyTask: PropTypes.func,
    50  showTask: PropTypes.func
    51};
    52
    53export default TableRow;

    Now let's commit these changes:

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