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.
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
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
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
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
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
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
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.
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
.
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.
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.
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"