The overall goal of this assignment is to assess your ability to implement:
- Nested resources
- Authenication
- Authorization
- Pagination
The functional goal of this assignment to implement a web application to manage TodoItems.
-
Start with the application completed in module 2. This will have defined all the models and relationships required for this assignment:
- User
- TodoList
- TodoItem
An Entity Relationship (ER) diagram is provided below to help depict each Model's relationship:
+------+ 1 * +----------+ 1 * +----------+ | User |--------| TodoList |--------| TodoItem | +------+ +----------+ +----------+
We will not be using the optional
Profile
model class in this assignment. -
Implement access to
TodoItem
as a nested resource ofTodoList
-
Lock down the application to only authenticated users.
-
Limit access to resources associated with the logged in user.
-
Access unbounded collections of resources using pagination.
-
Start with a copy of your
todolists
solution from the module 2 assignment -
From your
todolists
application root directory, remove the unit test from module 2.`-- spec `-- assignment_spec.rb
$ rm spec/assignment_spec.rb
-
Download and extract the starter set of boostrap files for this assignment.
|-- Gemfile |-- db | `-- seeds.rb `-- spec -- start.rb -- nested_resources.rb -- security.rb -- authorization.rb -- authentication.rb -- pagination.rb -- end2end.rb
-
Overwrite your existing Gemfile with the Gemfile from the bootstrap fileset. They should be nearly identical, but this is done to make sure the gems and versions you use in your solution can be processed by the automated Grader when you submit. Any submission should be tested with this version of the file.
NOTE the Gemfile includes a section added for the last assignment:
group :test do gem 'rspec-rails', '~> 3.0' gem 'capybara' end
as well as a new definition for the following items:
-
bcrypt
gem uncommented for use withhas_secure_password
-
tzinfo-data
gem conditionally included on Windows platforms -
will_paginate
added for implementing paging# Use ActiveModel has_secure_password gem 'bcrypt', '~> 3.1.7' # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] gem 'will_paginate', '~> 3.0.6'
-
-
Overwrite your existing
db/seeds.rb
file from the bootstrap fileset. This file contains some test data that will be useful during development and unit tests. -
Add the
spec/*.rb
files provided with the bootstrap fileset to the correspondingspec/
directory within yourtodolists
application. These files contain tests that will help determine whether you have completed the assignment.
-
-
Run the
bundle
command to make sure all gems are available.$ bundle
-
Run the rspec test(s) to receive feedback.
rspec
must be run from the root directory of your application. There are several test files provided for this assignment. Many of those files are designed to test your code at specific points as you proceed through the technical requirements of this assignment. As such, many tests will fail if executed after additional technical requirements have been completed. Initially, majority of tests will (obviously) fail until you complete the requirements necessary for them to pass.$ rspec ... (N) examples, 1 failure, (N) pending
To focus test feedback on a specific step of the requirements, add the specific file (path included) with the tests along with "-e rq##" to the rspec command line to only evaluate a specific requirement. Pad all step numbers to two digits.
$ rspec spec/start_spec.rb -e rq1.0 ... (N) example, 0 failures
-
Implement your solution to the technical requirements and use the rspec tests to help verify your completed solution.
-
Submit your Rails app solution for grading.
-
Starting with a copy of your module 2 solution, this solution should already have
User
,TodoList
, andTodoItem
models defined with the following properties and relationships. This assignment does not use theProfile
model class but it will not hurt to include it.-
User
- username - a string to hold account identity
- password_digest - a string to hold password information
- todo_lists - a 1:many relationship with
TodoList
(i.e., Userhas_many
todo_lists).
Add appropriate options to have the
User
model class delete aTodoList
in a cascading fashion -
TodoList
- list_name - a string name assigned to the list
- list_due_date - a date when todo items in the list are to be complete. This is a date. We are not concerned with the time of day.
- user - a many:1 relationship with
User
(i.e., TodoListbelongs_to
User) - todo_items - a 1:many relationship with
TodoItem
(i.e., TodoListhas_many
todo_items).
Add appropriate options to have the
TodoList
model class delete aTodoItem
in a cascading fashion -
TodoItem
- due_date - date when the specific task is to be complete
- title - a string with short name for specific task
- description - a string with narrative text for specific task
- completed - a boolean value (default=false), indicating whether item is complete
- todo_list - a many:1 relationship with TodoList - TodoItem belongs_to TodoList
$ rake db:migrate $ rspec spec/start_spec.rb
-
-
Add
has_secure_password
to theUser
model class. This will define apassword
property that will get processed into an encrypted hash stored in thepassword_digest
database column. We won't use this capability immediately -- but it is necessary to define it early in the assignment so that the data model works with thedb/seeds.rb
file in the next step.$ rspec spec/security_spec.rb -e rq02
-
Seed the database with the
db/seeds.rb
file. This will load sampleUsers
,TodoLists
, andTodoItems
. If this loads correctly -- your models and database are setup correctly and you are ready to start accessing the data through web pages produced by the controller and views.$ rake db:seed $ rails c > User.first.todo_lists.count => (N>0)
-
Use the
rails g scaffold_controller
command to create controller and view artifacts forTodoLists
andTodoItems
.$ rails g scaffold_controller TodoList list_name list_due_date:date $ rails g scaffold_controller TodoItem title due_date:date description:text completed:boolean
Update
config/routes.rb
to- Access
:todo_list
resources at URI/todo_lists
- Access
:todo_item
resources at URI/todo_lists/:todo_list_id/todo_items
- Make the
todo_list#index
action the root of the application
(Hint: refer to module 4, lesson 1, lecture:
Nested Resources: Part1
for details on how this is done)At this point,
TodoList
is defined as a global resource (with a root-level URI) andTodoItem
is defined as a nested resource, always scoped below theTodoList
it belongs to. Our application is not written to work that way, so expect some errors as we begin the modifications.If you have not yet done so -- please start the server and also take a look at your defined URI routes.
$ rails s #in separate console
$ rake routes #in original console $ rspec spec/nested_resources_spec.rb -e rq04
- Access
-
Update the
TodoList
to displayTodoItems
as a nested resource in thetodo_list#show
page (todo_lists/show.html.erb
).a. Copy the table from the
todo_items#index
page (index.html.erb
) and paste the table into thetodo_lists#show
page (todo_lists/show.html.erb
)b. Change global
@todo_items
references to scoped@todo_list.todo_items
references below:from: <% @todo_items.each do |todo_item| %> to: <% @todo_list.todo_items.each do |todo_item| %>
c. Remove the
Edit
link forTodoItems
d. Change the link_to parameters from global
todo_item
references to provide fully qualified[@todo_list, todo_item]
references as an array.from: <td><%= link_to 'Show', todo_item %></td> to: <td><%= link_to 'Show', [@todo_list, todo_item] %></td>
from: <td><%= link_to 'Destroy', todo_item, method: :delete, data: ... to: <td><%= link_to 'Destroy', [@todo_list, todo_item], method: :delete, data: ...
$ rspec spec/nested_resources_spec.rb -e rq05b
NOTE: This test case is for incremental testing only and WILL FAIL after authentication infrastructure is in place later in this assignment.
$ rspec spec/nested_resources_spec.rb -e rq05d
NOTE: This test case is for incremental testing only and WILL FAIL after authentication infrastructure is in place later in this assignment.
e. Add a link to create a 'New Todo Item'. (hint: Use the
link_to
andnew_todo_list_todo_item_path(@todo_list)
helpers to produce a link tag)$ rspec spec/nested_resources_spec.rb -e rq05e
Note how the
new_todo_list_todo_item_path(@todo_list)
is formed from what is produced inrake routes
.$ rake routes new_todo_list_todo_item GET /todo_lists/:todo_list_id/todo_items/new(.:format) todo_items#new
- we want to invoke
todo_items#new
when we create a newTodoItem
- that action is mapped to using the
/todo_lists/:todo_list_id/todo_items/new(.:format)
URI and GET method. We are required to supply a :todo_list_id new_todo_list_todo_item_path
is formed by adding_path
tonew_todo_list_todo_item
- the :todo_list_id is filled in by passing in a
@todo_list
when calling it - GET is provided by
new_todo_list_todo_item_path
Notice that the
TodoItems
now display on thetodo_list#show
page by navigating to a specificTodoList
. However, theTodoItem
URIs are not yet implemented in theTodoItem
controller (next step).$ rspec spec/nested_resources_spec.rb -e rq05
- we want to invoke
-
Modify the
TodoItem
controller to work as a nested resource by implementing the following. Note that your views withTodoItem
URI references will not work until these changes are made and the links and forms are updated to include the scopingTodoList
for each referencedTodoItem
. The unit tests, however, will be able to make calls into your back-end to determine all URIs are implemented properly -- prior to moving on to the views.a. Remove the old URI comments or replace them to have the following form since all calls to a
TodoItem
will now be scoped below aTodoList
. Use the todo_item output ofrake routes
to give you a head start.$ rake routes #METHOD /todo_list/:todo_list_id/todo_items #METHOD /todo_list/:todo_list_id/todo_items/:id
b. Remove the
todo_item#index
method andviews/todo_items/index
pages. This will no longer be called since allTodoItem
displays will be scoped to a particularTodoList
. We will get theTodoList
and calltodo_list.todo_items()
instead.c. Add a private helper method called
set_todo_list
that sets the@todo_list
instance variable from the:todo_list_id
property passed in via the params. (Hint: try the following in therails console
if you need practice locating a TodoList by id)$ rails c > list_id=TodoList.first.id > @todo_list=TodoList.find(list_id)
d. Update the private helper method called
set_todo_item
to scope its find command to thetodo_items
of a specific@todo_list
list. (Hint: try the following in therails console
if you need practice locating aTodoItem
by id scoped to aTodoList
)$ rails c > list_id=TodoList.first.id > @todo_list=TodoList.find(list_id) > item_id=@todo_list.todo_items.first.id > @todo_item=@todo_list.todo_items.find(item_id)
e. Invoke the
set_todo_list
method before each method in the controller is executed using abefore_action
f. Update the
todo_item#new
action to return a newTodoItem
instance that is initialized to reference its parent@todo_list
, which is provided byset_todo_list
.
(Hint: try the following therails console
if you need practice creating a new instance of aTodoItem
associated with aTodoList
. Notice the new TodoItem is never saved to the database during this call. However, what is passed back to the form is a TodoItem prototype that has its foreign key reference set to theTodoList
so thatTodoList
can be referenced when theTodoItem
is finally created in a follow-on POST)$ rails c > @todo_list=TodoList.first > @todo_item=@todo_list.todo_items.new
g. Update the
todo_item#create
to create a newTodoItem
instance based on thetodo_item_params
as before. Except now create this instance associated with the@todo_list
provided byset_todo_list
. (Hint: try the following in therails console
if you need practice creating a new instance of aTodoItem
associated with aTodoList
. Notice that in this case --save()
is being called on thetodo_list
, causing the newTodoItem
to be inserted into the database.)$ rails c > @todo_list=TodoList.first > @todo_item=@todo_list.todo_items.new(title:"my item") > @todo_list.save()
h. Update the HTML redirect of the
todo_item#create
,todo_item#update
, andtodo_item#destroy
methods to do to thetodo_list#index
page. (Hint: use the@todo_list
variable withinredirect_to
to express thetodo_list#index
page URI) -
Update
TodoList
andTodoItem
views to adjust the links and forms in these views to work with the updated URIs andTodoItem
controller.a. Update the links on the
todo_items#show
page (todo_items/show.html.erb
) to include theTodoList
theTodoItem
is a member of.-
Change the
Edit
link_to path parameter from the globaledit_todo_item_path
(that no longer exists) to the newedit_todo_list_todo_item_path
. This new method requires both@todo_list
and `todo_item' passed in as separate arguments (not as an array -- as in previous requirement).from: <%= link_to 'Edit', edit_todo_item_path(@todo_item) %> | to: <%= link_to 'Edit', edit_todo_list_todo_item_path(@todo_list, @todo_item) %> |
-
Change the
Back
link_to path parameter from to globaledit_items_path
(that no longer exists) to thetodo_listi#show
page it is a member of. This requires using the@todo_list
.from: <%= link_to 'Back', todo_items_path %> to: <%= link_to 'Back', @todo_list %>
$ rspec spec/nested_resources_spec.rb -e rq07a
b. Update the links on the
todo_items#edit
page (todo_items/edit.html.erb
) to include theTodoList
theTodoItem
is a member of.-
Change the
Show
link_to path parameter from a global@todo_item
reference to include its@todo_list
. This requires using both@todo_list
and `@todo_item' passed in as separate arguments as an array.from: <%= link_to 'Show', @todo_item %> | to: <%= link_to 'Show', [@todo_list, @todo_item] %> |
-
Change the
Back
link_to path parameter from a globaltodo_items_path
(that no longer exists) to reference theTodoList
it is a member of. This new method requires the@todo_list
passed in as a single argument.from: <%= link_to 'Back', todo_items_path %> to: <%= link_to 'Back', @todo_list %>
c. Update the form parameters on the
TodoItems
form partial page (todo_items/_form.html.erb
) to include theTodoList
theTodoItem
is a member of.-
Change the link_to parameters from global
todo_item
references to provide fully qualified `[@todo_list, @todo_item]' references as an array.from: <%= form_for(@todo_item) do |f| %> to: <%= form_for([@todo_list, @todo_item]) do |f| %>
$ rspec spec/nested_resources_spec.rb -e rq07c
d. Update the links on the
todo_items#new
page (todo_items/new.html.erb
) to include theTodoList
.-
change the
Back
link_to path parameter from a globaltodo_items_path
(that no longer exists) to reference theTodoList
it is a member of. This new method requires the@todo_list
passed in as a single argument.from: <%= link_to 'Back', todo_items_path %> to: <%= link_to 'Back', @todo_list %>
$ rspec spec/nested_resources_spec.rb -e rq07d
e. Make the display of
completed
conditional on thetodo_item
being edited versus new. Users should not be allowed to see/change thecompleted
property for a newTodoItem
. (Hint: edited objects are persisted and can be tested usingpersisted?
. Objects can also be tested with.new_record?
)$ rspec spec/nested_resources_spec.rb -e rq07e
$ rspec spec/nested_resources_spec.rb -e rq07
-
-
Verify that you have implemented a password login capability for the
User
model. You implemented this in an earlier step to allow the provideddb/seeds.rb
to immediately work with passwords. This this should just be a sanity check and review of howhas_secure_password
works.-
Using the
rails console
, verify that you fail authentication when using the wrong password for a specificUser
. You can locate theusername
and assignedpassword
in thedb/seeds.rb
file.$ rails c > user=User.where(username:"rich").first > user.authenticate("wrongpassword") => false
-
Using the
rails console
, verify that you can authenticate using a valid password for a specificUser
.> user=User.where(username:"rich").first > user.authenticate("123abc") => #<User id: 277, username: "rich", password_digest: "$2a...
-
Using the
rails console
, verify that you can authenticate and get theTodoLists
for an authenticatedUser
.> user=User.where(username:"rich").first > user.authenticate("123abc").todo_lists.count => 49 #seed data randomly generated
$ rspec spec/security_spec.rb -e rq08
-
-
Create a new controller to manage the user's
session
when interacting with the server.a. Use the
rails g controller
command to create aSessions
controller with the following actions:- new
- create
- destroy
b. Clean up the
config/routes.rb
file edited by therails g controller
command to be the following:-
generated:
get 'sessions/new' get 'sessions/create' get 'sessions/destroy'
-
change to:
resources :sessions, only: [:new, :create, :destroy]
c. Map the GET
/login
action tosessions#new
inconfig/routes.rb
. Have this be referred to as thelogin
resource sorake routes
generates alogin
_path helper.get "/login" => "sessions#new", as: "login"
d. Map the DELETE
/login
action tosessions#destroy
inconfig/routes.rb
. Have this referred to as thelogout
resource sorake routes
reports alogout
_path helper.delete "/logout" => "sessions#destroy", as: "logout"
$ rspec spec/security_spec.rb -e rq09
-
Implement the
Sessions
controller class and view. This should permit a caller to willingly navigate to the/login
page, login with a correct password, and proceed to the root URI. Nothing will stop an un-authenticated user from accessing the same list at this time. (Hint: the information to complete this step is contained in module 4, lesson 2, lecture:Sessions and Controller View
)a. Leave the
new
method in its default state. This will cause the route to continue straight toviews/sessions/new.html.erb
.b. Update the
sessions#new
page (views/sessions/new.html.erb
) to declare a form:-
for a
User
model type :user; this will cause the properties of the form to be assigned to an instance of aUser
-
with a
sessions_path
URI; this will cause a POST to the sessions#create URI to be invoked when a submit is called -
with a :username text_field; this will assign the user input to the
user[username]
property -
with a :password password_field; this will obfuscate the user's password while being typed and assign the user input to the
user[password]
property -
with a submit action; this will submit the form to the server when pressed
<h1>Login</h1> <%= form_for(:user, url: sessions_path) do |f| %> <div class="field"> <%= f.label :username %> <br/> <%= f.text_field :username %> </div> <p/> <div class="field"> <%= f.label :password %> <br/> <%= f.password_field :password %> </div> <p/> <div class="actions"> <%= f.submit "Login" %> </div> <% end %>
$ rspec spec/security_spec.rb -e rq10b
c. Implement the
create
method as follows:- get the user's
username
andpassword
from the submitted form - find the user based on
username
- authenticate the user using the supplied
password
- if authenticated
- store the user.id in the
session
- redirect the caller to the
root_path
of the application and supply a flash.notice message announcing successful login
- store the user.id in the
- if not authenticated
-
redirect the caller to the
login_path
and supply an flash.alert message$ rspec spec/security_spec.rb -e rq10c
-
d. Implement the
destroy
method as follows:-
reset the
session
, wiping out the user's session and everything in it -
redirect the caller to the
login_path
with a flash.notice message announcing successful logout$ rspec spec/security_spec.rb -e rq10d
e. Remove the
destroy
andcreate
pages in the view, generated byrails g controller
since they are not being used.$ rspec spec/security_spec.rb -e rq10d
-
-
Require users to authenticate with your application prior to accessing anything except the login page. At the completion of these steps, no one should be able to access anything except the login page -- until they successfully authenticate. (Hint: the information to complete this step is contained within module 4, lesson 2, lecture:
Authorization
)a. Define a
logged_in? helper method in the
ApplicationControllerclass that evaluates to
true` if the there is a user associated with the session.b. Define a
current_user
helper method in theApplicationController
class that finds and returns theUser
instance associated with the session.c. Expose
logged_in?
andcurrent_user
as helper methods outside of the controller usinghelper_method
. Note these methods were already available to all controller sub-classes. This designation makes them available to the views as well.d. Define a
ensure_login
helper method in theApplicationController
class that redirects the caller to thelogin_path
if they are not logged in. Note this method is available to all controller sub-classes in the application.e. Define that all methods perform
ensure_login
before they are called usingbefore_action
.f. Create an exception to the above rule so that
sessions#new
andsessions#create
can be accessed by an unauthenticated user -- otherwise no one will be able to access the login page.class SessionsController < ApplicationController skip_before_action :ensure_login, only: [:new, :create]
g. Update the
views/layouts/application.html.erb
page to include user/logout information based on the current session state. With this snippet in place -- you should be able to login and see the current_user.username displayed in the right, top corner of the display.<% if logged_in? %> <div style='float: right;'> Logged in as <%= current_user.username %> | <%= link_to "Logout", logout_path, method: :delete %> </div> <% end %>
$ rspec spec/authentication_spec.rb -e rq11
-
Update the application so that authenticated users can only have access to
TodoLists
associated with their specific user. This mostly involves updating theTodoListController
to change all globalTodoList
commands to be scopedcurrent_user.todo_lists
commands. (Hint: If you need some practice accessingTodoLists
for an authenticated user, try the following commands in therails console
.$ rails c > user_id=User.where(username:"rich").first.id > current_user=User.find(user_id) > current_user.todo_lists.count => 49 #random assignment -- some number greater than 0
This mostly involves changing the following from/to.
from: TodoList.x to: current_user.todo_lists.x
At this point, logged in users should only be able to see their
TodoList
s$ rspec spec/authentication_spec.rb -e rq12
-
Add pagination to your application to help scale and manage methods that can return unbounded collections of information.
a. Verify the
will_paginate
gem is added to your Gemfile.b. Update the
todolist#index
action to return a page ofTodoLists
associated with thecurrent_user
that are up to 8 objects per_page. (Hint: If you are not familiar with howwill_paginate
works, you can get some familiarity using therails c
and Active Record commands.will_paginate
adds an additional method to all model classes to be able to break find command results into pages and page results.)$ rails c > 3.times {|n| p TodoList.paginate(page:n+1, per_page:1)} > p TodoList.paginate(page:1, per_page:1).total_pages => 101
The page number will be available in the
params[:page]
property of the call.c. Add
will_paginate
to yourtodolists#index
page and apply it to your@todo_lists
result from the controller.At this point, logged in users should only be able to see their
TodoList
s and$ rspec spec/pagination_spec.rb
-
Perform an end-to-end check of your work. Before you do, you must remove the confirmation dialogs from your Destroy links since we are not using a webdriver that supports javascript for this assignment. Inside the todo_list show.html.erb file you will need to change the destroy link to eliminate the confirmation dialog:
from: <%= link_to 'Destroy', [@todo_list, todo_item], method: :delete, data: { confirm: 'Are you sure?' } %> to: <%= link_to 'Destroy', [@todo_list, todo_item], method: :delete %>
Do the same for todo_list index.html.erb
from: <%= link_to 'Destroy', todo_list, method: :delete, data: { confirm: 'Are you sure?' } %> to: <%= link_to 'Destroy', todo_list, method: :delete %>
a. Login to the application as
rich
b. Access the first
TodoList
on the second pagec. Complete the first
TodoItem
in thatTodoList
if not completedd. Create a new
TodoItem
for thatTodoList
e. Delete a
TodoItem
from thatTodoList
f. Create a new
TodoList
g. Delete a
TodoList
rspec spec/end2end_spec.rb
Some unit tests have been provided in the bootstrap files and provide examples of tests the grader will be evaluating for when you submit your solution. They must be run from the project root directory.
$ rspec (file)
...
(N) examples, 0 failures
You can run as many specific tests you wish be adding -e rq## -e rq##
$ rspec (file) -e rq01 -e rq02
Note that some of the earlier specs cannot be run once security has
been fully enabled. Use the end2end
test when complete. Each of the
individual requirements list specific specs that can be used during
the time of that development.
Submit an .zip archive (other archive forms not currently supported) with your solution root directory as the top-level (e.g., your Gemfile and sibling files must be in the root of the archive and not in a sub-folder. The grader will replace the spec files with fresh copies and will perform a test with different query terms.
|-- app
| |-- assets
| |-- controllers
| |-- helpers
| |-- mailers
| |-- models
| `-- views
|-- bin
|-- config
|-- config.ru
|-- db
|-- Gemfile
|-- Gemfile.lock
|-- lib
|-- log
|-- public
|-- Rakefile
|-- README.rdoc
|-- test
`-- vendor