Week 9 ½ — HTTP Verbs & Query Parameters — To Do List

Share on facebook
Share on google
Share on twitter
Share on linkedin

Week 9 ½ — HTTP Verbs & Query Parameters — To Do List

A screenshot of my improved to do list Sinatra app

This is a continuation of my blog post from week 7, where I created a to do list web app using Sinatra. I apologise if this blog is a bit all over the place. The purpose is to be a reminder for myself on how I implemented the tasks set by a mentor to improve the app.

To be honest, the app I created by the end of week 7 probably wasn’t very user friendly, but it was functional. During week 9, I asked one of my mentors, Laurent, for feedback on how to improve. The main suggestions were to look into:

  • REST
  • HTTP verbs
  • Query parameters

REST & HTTP Verbs

Representational State Transfer (REST) is a software architectural style that defines a set of constraints to be used for creating web services.

— Source: https://en.wikipedia.org/wiki/Representational_state_transfer

The HTTP verbs comprise a major portion of our “uniform interface” constraint and provide us the action counterpart to the noun-based resource.

The primary or most-commonly-used HTTP verbs (or methods, as they are properly called) are POST, GET, PUT, PATCH, and DELETE. These correspond to create, read, update, and delete (or CRUD) operations, respectively.

— Source: https://www.restapitutorial.com/lessons/httpmethods.html

An example from https://guides.rubyonrails.org/routing.html

Although my to do list app had options to delete, edit, mark it as complete or incomplete, the only HTTP verbs currently being used were GET and POST. In addition, if a user wanted to implement one of the options to a todo, they would be redirected to a new route, and would have to select the number corresponding to the todo they wished to edit. Quite long winded! ?

My mentor recommended ensuring each todo had their own action buttons. As a result, the user could simply select an action on the same page (excluding editing a todo), right next to the todo of their choice.

Using the DELETE HTTP Verb

As an example, here’s what the delete todo action was like previously:

get "/delete_todo" do    
erb :delete_todo
end
post "/delete_todo" do
TODOS.delete_todo(params[:id])
redirect "/"
end

The user would be redirected to delete_todo.erb, where they would need to select the index corresponding to the todo they wished to delete, and then click on a submit button. A combination of GET and POST HTTP verbs were used to make this possible.

To make this option RESTful, the DELETE HTTP verb would be used. In order to identify which todo was to be deleted, the route would contain a parameter, which would be replaced with something unique to the todo item.

In this case, the :id parameter corresponds to the index of the item in the array of todos:

delete "/todos/:id" do
TODOS.delete_todo(params[:id])
redirect "/"
end

In the index.erb file, I used the each_with_index method to display the todos, and get the index (id) of each todo:

<% @status.each_with_index do |todoentry, id| %>
...
<% end %>

Within this, the index was used as the form action value, to specify a unique route for the selected todo item. For example, the first item in the array has an index of 0, therefore the action would be /todos/0. For the second item, it would be /todos/1, and so on.

action="/todos/<%= id %>">
...

Forms only accept get and post as the method. To get around this, an input with the type hidden had to be added to be able to use the DELETE verb




Now how did this all work together? Here’s the delete_todo method being called via the delete “/todos/:id” route:

def delete_todo(id)
@todos.delete_at(id.to_i)
end

When the delete button is clicked next to a todo, the delete_todo method is called with the index of that specific item as the argument. As a result, it is deleted from the array of todos.

User can delete a todo on the same page

Using the PUT verb to edit a todo item

Previously, if a user wished to edit a todo, they would need to click on the edit button, which would take them to the edit page. Like the delete option, on the edit page, they would need to select the index of the todo.
Below that, in the text field, they would need to write what they would like to edit it to, then click submit. Once again, another long winded process ?

My mentor suggested each todo having it’s own edit button. When clicked, it would redirect to an edit page for that specific todo item.
On this page, the text box would already contain the current description of the todo as the value, which the user could edit. When edited, the user clicks on the submit button to apply the changes.

The goal — each todo item having its own edit button

Previously, to edit a todo item, the GET and POST verbs were used:

get "/edit_todo" do
erb :edit_todo
end
post "/edit_todo" do
TODOS.edit_todo(params[:id], params[:todo],)
redirect "/"
end

As the user would still need to be redirected to an edit page, the GET verb (with some changes of the route) would need to remain. However, the POST verb would need to be replaced with the PUT verb. PUT is used to update/replace.

In order to identify which todo was to be edited, the route would need to contain a parameter, which would be replaced with something unique to the todo item. In this case, :id corresponds to the index of the item in the array.

get "/todos/:id/edit" do
...
end

As with the delete option, within the each_with_index method in index.erb, the :id param would be replaced by the index of each todo. As a result each route for editing a todo is unique.

"/todos/<%= id %>/edit">

Clicking on the button redirects the user to the edit page for the selected item, but with a unique url.

get "/todos/:id/edit" do
erb :edit
end
------
URLs:
1st item in the list: http://localhost:9292/todos/0/edit?
2nd item in the list: http://localhost:9292/todos/1/edit?

The unique identifier (:id / item index) would also need to be used when using the PUT verb:

put "/todos/:id/edit" do    
TODOS.edit_todo(params[:id], params[:todo],)
redirect "/"
end

In addition, it would need to be used as an argument when calling the edit_todo method, along with what the todo description is being edited to.

def edit_todo(id, todo)
@todos[id.to_i][:todo] = todo
end

Moving onto the edit.erb page, a form is present which allows the user to edit the todo description. Once again, forms only accept get and post as the value or method. To get around this, an input with the type hidden had to be added to be able to use the PUT verb:

@id %>/edit">

Previously, using the each_with_index method allowed access to the index of each item in the list. However, as there’s no need to show all of the todos on the edit page, this method would be redundant.

In order to get the unique id of the selected todo, I created an instance variable @id, with the value as the params[:id]. For example, for the first item in the list, the params[:id] would be 0.

The @id instance variable was then injected into the action value for the form:

in app.rb: 
get "/todos/:id/edit" do
@id = params[:id]
erb :edit
end
---
in edit.erb: 
@id %>/edit">

In the form, there is a text box where the user can enter the new description for the todo. A suggestion by my mentor was to have the text box pre-filled with the current description of the todo.

To do this, in the get “/todos/:id/edit” route, I created an instance variable @todotext, with value as the current todo description. This was then injected in the erb file, to be the value of the text box:

in app.rb: 
get "/todos/:id/edit" do
@id = params[:id]
@idnum = @id.to_i
@todotext = @todos[@idnum][:todo]
erb :edit
end
--
in edit.erb:
@todotext %>" autofocus required>
The edit feature in action
NOTE: route contains “puts params”. As a result, params are printed. If Sinatra logger is being used, “logger.info params” does the same

Filtering the todos using query parameters

Previously, the app contained the following pages: view_completed_todos.erb and view_incomplete_todos.erb. Within these, Ruby was injected, and a each_with_index method was used to only show items where the value of :completed was true, or was false:

<% TODOS.todos.each_with_index do |todoentry, index| %>
<% if todoentry[:completed] == true %>

<%= index + 1 %>:
<%= todoentry[:todo] %>


<% end %>
<% end %>

I needed to achieve the following:

When filtering todos, the resulting URL should be RESTful.
Example: when I filter for completed todos, the URL should be /?status=completed.

I started by creating a method within the ToDoList class that could return either:

  • all todo items (completed and incomplete)
  • completed todo items only
  • incomplete todo items only

Currently, when a todo item is created, it is created as a hash, containing a key named :complete. The value of this is set to false. If marked as complete, the value is changed to be true.

def filter_todos(filter)
value = nil

if filter == "complete"
value = true
elsif filter == "incomplete"
value = false
end
  if value == true || value == false
@todos.select do |todo|
todo[:completed] == value
end
else
@todos
end
end

The method would need to take a filter as an argument, which would determine what is returned from the array of todos.

As the argument inputted would not be true or false, but either incomplete, complete or all, as a string (the submit button’s value), these would need to be converted into either true or false for the incomplete or complete todos to be returned.

In the instance that the value was “all”, then the array of todos (@todos) would be returned.

In layout.erb, I set the different filter options as buttons, with the value either as all, incomplete or complete, and the name value as “status”. The resulting URL from clicking the buttons is RESTful.


  • value="all" name="status">
  •      
  • value="incomplete" name="status">
  •       
  • value="complete" name="status">

  • ----
    URL after clicking on "complete" button:
    http://localhost:9292/?status=complete

    The above created a status parameter, with the value set to the value of the input. As a result, this is what is inputted as the argument when calling the filter_todos method.

    get "/" do
    ...
    @status = TODOS.filter_todos(params[:status])
    ...
    end

    What is returned is set to an instance variable @status. To reiterate, status would either be an array with:

    • all todos
    • only completed todos
    • only incomplete todos
    <% @status.each_with_index do |todoentry, id| %>
    ...
    <% end %>
    Using query params to filter todos

    Things I’ve learned

    • How to use the PUT and DELETE HTTP verbs
    • Using “puts params” or the sinatra custom logger to view params

    Things I’ve struggled with

    • Getting to grips with query parameters

    Further features & goals to implement:

    • Giving each todo a unique ID and storing todos in a hash
    • Store todos in Postgres
    • Add “complete by date” to todo
    • User can filter and view “Overdue” todos
    • User should be able to have multiple todo lists
    • Deploy to Heroku ??‍♀️