MyComicsBox- A Sinatra CRUD App

Posted by Andrew Nikonchuk on August 30, 2018

For the Sinatra project for the Flatiron School’s Online Full Stack Web Development Program, we were tasked with creating any sort of CRUD app using Sinatra that had the ability to create, read, update, and delete items. This simple Content Management System needed to use MVC (Model, View, Controller) architecture with ActiveRecord integration. We needed to integrate secure user accounts and validations for user input to ensure bad data isn’t added to the database.

I decided to focus my project on comic book collections and create “MyComicsBox”. As a comics nerd at heart, I knew I would be able to leverage my interest in the subject to create a system for cataloging a personal comics collection and sharing that data with other users. The key to designing the application would be to ensure that the scope of the data being processed would be robust without being overwhelming. There is an abundance of information about an individual comic book, from writers to artists to colorists to editors. How do you organize the data and choose what to include without creating an unreasonably complex data structure?

Models and Database Structure

In order to achieve a clear, organized structure, I decided to limit my data about each individual book to writer and artist. This would result in four distinct models in my architecture:

  • Comic- The “Comic” model is the central aspect of this data structure. Comics have a name, which includes the title of the comic and the issue number, and they would belong to a Writer and an Artist, as well as to an individual User. In addition, there is a data field for “notes”, so the User who creates the Comic can add any additional information they see fit, such as significant events or whether it is part of a miniseries or crossover event.
  • Writer- In this model, writers have a name. If more than one writer is responsible for a book, they will be represented by a single name, like a team. Writers have many Comics, and they have many Artists through the Comic model. They will only be created when a User creates a Comic.
  • Artist- Like Writer, the Artist model consists of artists with a name. They also have many Comics, and they have many Writers through the Comic model. They will also only be created when a User creates a Comic.
  • User- The User model is responsible for the data in this architecture. Users have a username and secure password. They have many Comics, and they have many Artists and Writers, both through the Comics model. Individual users are able to add a new Comic, and they have access to edit and delete individual Comics that they create.

Planning Views and Routes/Controller Actions

As the Models grew more and more complex, I knew that I needed to start planning individual views. What should a visitor to the app be able to see? Who should have access to what? After considering all the information I was collecting, I settled on the following views, adhering to RESTful routes as much as possible. Each Model would have its own Controller, responsible for handling the logic of displaying the views.

General Views- Application Controller

  • Root Route (“/“)- This view has an overview of MyComicsBox and links to register as a new user or sign in. Along with the registration and login pages, this will be the only page users who are not logged in will be able to see.

User Views- Users Controller

  • Register (“/register”)- The registration page has a simple form for collecting a username and a password. The form will send a post request to “/register” to create the new user and validate that their username is unique and the password is present. If successful, users will be redirected to their User show page. If not, they will be redirected to the registration page.
  • Log In (“/login”)- The login form has a simple form with username and password that will send a post request to “/login” to sign in the user. If authenticated (see “Securing Users”, below), users will be redirected to their User show page. If not, they will be redirected to the Login page.
  • Log Out- While not a view page itself, there will be a “Log Out” link on every page once a user is logged in. This will end the user’s session by sending a get request to “/logout”, clearing the sessions hash, and redirecting the user to the “/” root route.
  • User Show Page (“/users/:slug”)- Each individual user will have his or her own “show” page that lists the comics in their collection (with links to individual comics show pages), their writers (with links to the writers’ show pages), and their artists (with links to the artists’ show pages). Instead of using a User’s ID from the database to access the show page, I used the slug of their username (to eliminate spaces and downcase the username). This page will only be viewable to logged in users.

Comics Views- Comics Controller- Users will have to be logged in to see any of these pages.

  • Comic Index Page (“/comics”)- This page lists all the Comics from all the users, along with links to the individual Comic’s show page.
  • Comic Show Page (“/comics/:id”)- I decided to use the database ID in the URL instead of slugifying the title because the URL would end up being unwieldy and long. I made the same decision with the Artist and Writer show page URL’s. The individual Comic show page lists the User who added it to the collection, the Writer, and the Artist, all with links to the show pages for the individuals. If the user who is logged in is the one who created the Comic, it also displays a link to the Edit page and a link to Delete the book.
  • Comic Create Page (“/comics/new”)- This page contains a form to create a new comic that sends a post request to “/comics”. The user provides the name of the Comic, any notes about the comic, and fills in a text field for the Writer and the Artist. In the Comics Controller, the database will be queried using a find_or_create_by request to ensure that the writer and artist are not repeated in the database. The current User is also associated with the new Comic, so that the new comic has a Writer, an Artist, and a User when it is saved to the database. Upon successful saving, the User will be redirected to the Show page for the new Comic.
  • Comic Edit Page (“/comics/:id/edit”)- This page contains a form for the user who created the Comic to edit all information about the comic. It will send a Patch request to “/comics/:id” (see “Patch and Delete Requests” below). After successfully editing the Comic, the user is redirected to the individual Show page for the Comic.
  • Comic Delete- The Delete action is not accomplished through a page itself, but instead by using a button on the show page for the individual Comic, only visible to the user who created the Comic. This simple form will send a Delete request to “/comics/:id” (see “Patch and Delete Requests” below). Users will be redirected to their User show page upon successful deletion.

Writer and Artist Views- Writers Controller and Artists Controller- Users will have to be logged in to see any of these pages

  • Index Pages (“/writers” and “/artists”)- These will list all the writers and artists in the database respectively and contain links to the individual show pages.
  • Show Pages (“/writers/:id” and “/artists/:id”)- These will list the books that the individuals have created, along with associated artists or writers, respectively, with links to their individual show pages.

Getting Started

I used the corneal gem to set up my initial file structure. This gem, created by a former Flatiron student, sets up a file tree that has all the necessary structure in place to just start coding. The config/environment.rb file connected ActiveRecord to a database and required all files in my app folder (with my Models, Views, and Controllers). The Gemfile also contained (almost) all the gems I would need to run my application, including sinatra, activerecord, sqlite, shotgun, and bcrypt for securing my users. The only gem I needed to add was the gem for displaying flash messages (see “Flash Messages Issues” below). There were a number of files, folders, and gems I needed to delete, like the rspec folder for testing, the testing gems, and the javascript folder, because I would not need these for my final product. Overall, the corneal gem was incredibly useful in setting up my project for success.

Securing Users

In order to secure Users’ passwords in my application, I used the open-source gem, bcrypt, to encrypt passwords. Instead of storing the password as plain-text, bcrypt provides a hashing algorithm that manipulates the password so it cannot be returned to its original state. It also adds a “salt”, or a random string of characters, to the hash, to further secure it. I stored the encrypted password in a database column called “password_digest” instead of simply password. I also had to add the macro “has_secure_password” to my User model in order to work in concert with bcrypt to secure the password. This adds a number of methods to my User model, including validating the presence of a password before the User is initially saved. In addition, it provides an #authenticate method that I used when logging Users in that transforms the password into a hashed version and compares it with the version stored in the database. If they match, it returns “true.” If not, it returns “false.” This ensures that Users attempting to log in have the appropriate credentials before I set their session[:user_id] that will signify that they are logged in.

Patch and Delete Requests

The forms for my Comics Edit page and the Comics Delete button must be submitted via POST requests because browsers do not know how to send PATCH or DELETE requests. However, by telling the config.ru file to use Rack::MethodOverride, part of Sinatra middleware, I was able to build in hidden fields in my forms to help my Controllers deal with PATCH and DELETE requests. In my Edit form, I added the following input, which allows me to override the post request in my “method” attribute of the form: <input id=“hidden” type=“hidden” name=“_method” value=“patch”>. This allowed my ComicsController to submit the form via a PATCH request instead of POST. Similarly, in my Delete form, I used the hidden input: <input id=“hidden” type=“hidden” name=“_method” value=“delete”>. This allowed my Controller to receive a DELETE request. These did not appear visible on the View itself. Rather, they are hidden inputs that simply sent additional information when the form was submitted. These workarounds employed the Rack::MethodOverride Sinatra middleware to interpret the name=“_method” attribute to translate the request to the value of the value attribute.

Refactoring to DRY Up Code

As I was planning and building out my models and views, I realized that I would need to sort the comics, the artists, and the writers alphabetically a number of times. Originally, I planned to do this using the following method: @comics = Comic.all (in my route in my controller) @comics.sort_by {|obj| obj.name} (on my view page) I was going to have to sort by name for a number of my views, sometimes multiple times in a view. For example, in the individual User’s show page, I would have to sort the comics, the writers, and the artists. Instead of repeating myself a number of times, I created a module (Sortable) in a concerns folder in my models, and extended a .sorted class method into each of my models. Initially, this worked for the Writer class, but caused my tux gem to throw an Uninitialized Constant error when extended into my Artist and Comic class. After pairing with 1 on 1 Portfolio Project Support, we troubleshot and realized that the concerns folder was being loaded after the Artist and Comic class alphabetically, so it was not being recognized. This was solved by renaming the folder “_concerns” so it would be loaded first. By creating this module and extending it into my Models, I was able to DRY up my code significantly and not repeat the #sort_by method each time I needed to sort.

Flash Messages Issues

As I built my Users Controller and Views, I built in flash messages to alert the user if their username wasn’t unique upon registration or if their login failed. I used the rack-flash3 gem in my Gemfile and followed the documentation to set up messages. When I initially tested the functionality, it all worked great. I went on to build flash messages into my Comics controller and create and edit views as well. Once again, upon testing, they worked. However, when I went back to my registration and login methods, the flash messages were no longer appearing. After working with Howard DeVennish during Office Hours, we switched from rack-flash3 to sinatra-flash for flash messages. After some quick revisions to formatting, everything worked great!

Reflecting on Progress and Going Forward

Building this web application has reinforced the curriculum’s Sinatra lessons by forcing me to adapt them to this new scenario. I have become much more comfortable with routing and with both authentication of users and authorization (who is allowed to view, access, and manipulate data). Looking at the project, if I were to propose any revisions, I would like to see the ability to add comments by other users to individual comics. I would also like to add images, especially individual comics covers, but that might be difficult due to copyright issues. However, I am proud of the work I have done as it stands. If you’d like to view my source code for the project, it is available at https://github.com/anikonchuk/my-comics-box.