This post showcases the web application I created as part of the curriculum for the Software Engineering program at Flatiron School. This application is an iteration of a simple content management system that implements CRUD (Create, Read, Update, Delete) functionality inside a MVC (Model, View, Controller) design paradigm. This application utilizes the web application library “Sinatra” to facilitate communication between components of the application and the web. Sinatra is built on Rack and provides the convenience of handling HTTP requests and responses. This app also utilizes ActiveRecord in order to provide an interface that allows the application to connect to a database using the relational database management system “SQLite3”. ActiveRecord aids in building database tables, connecting models to database tables and defining relationships between models.

Image for post
Image for post
NatParkRev provides user-generated reviews of National Parks.

Introduction

The application I built is called “NatParkRev” and its purpose is to provide a place for user-generated reviews of national parks. There are 62 national parks in the United States and no site is available that specifically tracks past and present user reviews for all of those parks. I get a lot of enjoyment visiting national parks so I thought it would be useful to have an application that allows people to read recent reviews of the park they intend on traveling to prior to their visit.

Image for post
Image for post
The main page for NatParkRev. Users are directed to login or sign up to see reviews.

Setup

There are ten Ruby gems required to run this application:

gem 'pry' — a runtime development console that allows the developer to freeze code on particular lines.
gem 'activerecord', :require => 'active_record' — facilitates the use of objects to be persisted to the database by providing methods and associations.
gem 'sinatra' — domain-specific language that handles HTTP requests and responses.
gem 'sinatra-activerecord', :require => 'sinatra/activerecord' — provides access to ActiveRecord helper methods and Rake tasks.
gem 'sinatra-flash' — provides a way to implement messages that are carried over to the redirected route.
gem 'rake'— allows common tasks to be created and run in the terminal.
gem 'shotgun'— provides automatic reloading of the rackup config.ru command by running shotgun.
gem 'bcrypt'— stores, salts and hashes user passwords.
gem 'sqlite3'— database management system.
gem 'require_all' — allows for one line to load all files and subdirectories inside a folder.

A Rakefile was created to manage tasks during development. In this file, the environment.rb file is loaded and the sinatra/activerecord/rake library is loaded to provide access to many useful rake tasks, including: db:create_migration, db:migrate, db:drop and db:seed.

require './config/environment'
require 'sinatra/activerecord/rake'
desc 'starts Pry with loaded environment'
task :console do
Pry.start
end

In order to setup the database, migration tables were created for :posts, :parks, :users, :characteristics, and :post_characteristics. The :post_characteristics table is a join table in which columns post_id and characteristic_id are foreign keys. By using the rake db:create_migration task command, a new file is created with the file name as the class name. These files represent the tables in the database. Each line below the create_table line of code below represents the columns in the database. The :posts table has six columns in it, including two foreign keys for the user and park id’s.

Image for post
Image for post
The migration table files are located in the ‘db/migrate’ directory of the application.

After all three migration tables are created, the rake db:migrate task command is run which actually creates the tables in the database.

A seed file was created in the database directory which loads in data to help aid when building the app. The seed file creates User, Post, Park and Characteristic objects, and associates the objects with each other.

Models

In order to associate objects with each other, ActiveRecord was used. ActiveRecord is the “M” or “model” part of the MVC paradigm. The model is responsible for the representation and manipulation of data in the application. The model is where the main logic of the application is configured. The controller will typically request objects from the model. The model will then request data from the database.

This application has five models: User, Post, Park, Characteristic, and PostCharacteristic. All models are represented as classes. Each model class inherits from ActiveRecord::Base which provides additional methods for these classes. The model class is where associations are created and validations are determined.

Users are associated with the other models as follows:
1. A User has many posts.
2. A User has many parks through posts.

Posts are associated with the other models as follows:
1. A Post belongs to a user.
2. A Post belongs to a park.
3. A Post has many post characteristics.
4. A Post has many characteristics through post characteristics.

Parks are associated with the other models as follows:
1. A Park has many posts.
2. A Park has many users through posts.

Characteristics are associated with the other models as follows:
1. A Characteristic has many post characteristics.
2. A Characteristic has many posts through post characteristics.

PostCharacteristics are associated with the other models as follows:
1. A PostCharacteristic belongs to a post.
2. A PostCharacteristic belongs to a characteristic.

A User is validated by the presence of the name, email and username attributes, in addition to the uniqueness of the user’s username. The has_secure_password macro is called which provides additional methods for validation, and include #authenticate, #password and #password=. This macro not only requires the presence of a password, but will authenticate the password against a password that is saved in the database which has been salted and hashed by bcrypt.

Image for post
Image for post
The User model class inherits from ActiveRecord::Base and includes presence validations for all columns in the :users database table.

There are two methods in the User model class. The first is the instance method #slug which transforms the username of an instance of User to a slugged version which yields a string of all lower case letters with hyphens inserted where there were once spaces. The second method is a class method .find_by_slug which iterates over all of the User instances and finds where the slugged username is equivalent to the slugged name that is being searched for. This method is useful when a user of the app is requesting a dynamic route that leads to /users/:username.

Views

The “V” or “view” part of the MVC paradigm is the user-facing portion of the application. This is what the user sees and interacts with as they navigate the app. There are three subdirectories within the views folder, which are: /parks, /posts and /users.

The main review page is located in the /posts subdirectory with a filename of index.erb. Views are written as .erb files which consist of HTML and embedded Ruby code. The main review page is the view where all user-generated reviews are displayed to the user of the application. It is important to note that while you do not have to be logged in to view this page, you are required to be logged-in in order to read the actual review.

Two drop down menus are displayed on this page which allows the user to filter reviews by a specific national park or state. Embedded Ruby code is denoted by the substitution tag, <%= and scripting tag, <%. The <select> html tag is utilized to create drop-down menus. The filter by park <select> form iterates over the @parks array, which is an array of all Park instances, Park.all. Each iteration will create an option with that specific park’s route as the value. Each iteration, aptly named park, gets the #slug method called on it which provides a streamlined route name. The drop-down menu display for each option is the park name plus the number of posts associated with that park. An unless statement is included so that if there are no posts with that park, the number of posts will not be displayed so it is easy for the user to discern which parks have reviews and which do not.

Image for post
Image for post
Users are able to filter reviews by park using a drop-down list that was created using the <select> tag.

The main review page also displays the actual table of every review in the database. This table iterates over the @posts array, which is an array of all created Post objects, Post.all. The posts are ordered by the datetime they were updated, in descending order, so that the most recent reviews are displayed at the top of the table. Each iteration creates a table row with various table data cells. Getter methods are called on each post which exposes and displays the necessary information.

Image for post
Image for post
The table displaying the list of reviews is populated by iterating over the @posts array.

Another important view page is posts/new.erb. This file holds the form for creating a new review. Users are required to fill in all fields on the form which include a title, national park, content and rating. There is also an optional checkbox field for characteristics. Since a timestamps column was included when creating the database table, the created_at and updated_at attribute is automatically populated when a new review is saved to the database. The content field uses the textarea html tag so that a multi-line text field is displayed. The name and value attributes will be available to the developer when the form is submitted via the params hash. The contents of the hash can be manipulated in various ways using nested hashes and arrays.

Image for post
Image for post
The new.erb file which displays the form for creating a new review.

By setting name="post[content]" and not setting the value (since the user will enter in the value), the params hash will be a hash with “post” as the key, along with a nested hash with a key of “content”. This allows the developer to gain access to all of these key/value pairs and use mass assignment when transforming this data into objects later on in the controllers.

Image for post
Image for post
An example of what the “params” hash looks like when a user hits the submit button.

The users/login.erb view displays a form to the user for when they want to log in to the application. This is a POST request as the user will be sending information back to the server. The username and password are requested of the user and this information is then validated once this request is received by the appropriate route and controller.

Image for post
Image for post
A simple user login form with required fields of “username” and “password”.

Controllers

Controllers are responsible for orchestrating the connection between the browser and the application. They are essentially conductors in an orchestra. Controllers receive requests from the client and routes the requests to the appropriate action. The controller then requests the data from the model, which in turn pulls data from the database. Once the models receive the data, the data is manipulated and sent back to the controller as objects. The controller then sends the objects to the view for rendering. The view will render the HTML which will be to the client as a response by the controller.

There are four controllers in this application: ApplicationController, ParksController, PostsController and UsersController.

The ApplicationController inherits from Sinatra::Base. All other controllers inherit from the ApplicationController which allows all controllers to have access to everything that ApplicationController possesses, including Sinatra::Base. The ApplicationController configures settings which control the features that get enabled. Cookie-based :sessions is enabled to allow users to stay logged into the application without re-entering credentials on every new page that’s loaded. The :views and :public_folder are set to designate specific directories for the location of the view and css/image files.

Image for post
Image for post
The ApplicationController configures settings and inherits from Sinatra::Base.

The ApplicationController also has helper methods defined to help the other controllers function properly and more efficiently. These include #logged_in?, #current_user, #redirect_if_not_logged_in, #redirect_if_not_post_owner, and #authenticate_and_change_password.

The #authenticate_and_change_password method allows a user to change their account information and utilizes the #authenticate method. The #authenticate method is provided by ActiveRecord::Base and is made available by calling the has_secure_password macro in the User model. When #authenticate is called on a particular instance of User, the password that is passed in (from the user’s input when the login form is submitted) is salted, hashed, and then compared to the salted and hashed password that is stored in the database. If the two version match each other, the return value will be that instance of User. If they do not match, then the return value will be false. At no point is the user’s password ever revealed since the password_digest column name in the users database table is encrypted with bcrypt.

Image for post
Image for post
The #authenticate_and_change_password method is called when a user submits a patch request to update their account information.

The PostsController is responsible for all requests pertaining to posts (reviews). When a new review is created, the post '/posts' route is called to action. This route will:

  1. Confirm that the user is logged in.
  2. Create a new instance of Post by passing in the params hash with the key of :post.
  3. Assign and associate the new post’s user to the current user.
  4. Save the newly created post and redirect if the post successfully saves.
Image for post
Image for post
The post ‘/posts’ route logic will save a new review to the database if certain conditions are met.

An example of a get request is the get '/posts/:id/edit' route. This route is responsible for displaying the edit form to the user and will:

  1. Confirm that the user is logged in.
  2. Find the particular post that is being requested to be edited by searching for the :id attribute in the database that corresponds to the params hash :id that is part of the dynamic url route.
  3. Confirm that the current user is associated as the owner of the post.
  4. Render the posts/edit view.
Image for post
Image for post
The get ‘/posts/:id/edit’ route will render an edit review form if certain conditions are met.

Once the edit form is filled out correctly, the user can submit the form. The action of the form designates which route is requested. The edit form will send a POST request to the controller, BUT in actuality this is a patch request. A “hidden” input field is created inside the <form> tag of the edit view which designates this request as a patch request instead of a traditional POST request.

Image for post
Image for post
A “hidden” input field changes this POST request to a patch request.

The associated patch '/posts/:id dynamic route is called to action which will:

  1. Confirm that the user is logged in.
  2. Find the particular post that is being requested to be edited by searching for the :id attribute in the database that corresponds to the params hash :id that is part of the dynamic url route.
  3. Confirm that the current user is associated as the owner of the post.
  4. Redirect to the /posts/:id route if the updated post saves in the database.
Image for post
Image for post
The “patch” request responsible for updating the database with changes the user made.

The UsersController is responsible for all routes pertaining to user actions. One useful route is the get '/users/:username' dynamic route which is a GET request to a particular user page. This route will:

  1. Confirm that the user is logged in.
  2. Find the particular user instance in the database via the dynamic :username url. The class method .find_by_slug is utilized here which iterates through the User.all array and searches for where the user’s username converted to slug form matches the passed in slug form of the username (which is pulled from the params hash).
  3. Render the /users/show page only if the user is found and the user’s username is equivalent to the session’s username.
Image for post
Image for post
The get request for a users dynamic route.

The post '/login' route utilizes the #authenticate method and will:

  1. Find the User instance by searching the database for username that the user entered in the form.
  2. Confirm that the user instance exists and that the password entered into the form matches the password in the database.
  3. Set the :username key of the session hash to that particular user’s username.
  4. Redirect to the previously sought after URL if one was set, or redirect to the user’s page if conditions are met.
Image for post
Image for post
The post ‘login’ route ensures user’s are authenticated in the database before logging them in.

The Final Product

CSS was implemented after coding the backend of the application. The layout.erb file holds the template HTML. A navigation bar was implemented so that only relevant options are displayed to the user, depending on if they are logged in or not. Flash messages were integrated into the controllers to provide the user with useful information when navigating the site. The navigation bar and flash message logic are found in the layout.erb file.

Image for post
Image for post
The flash message will display various messages to the user depending on the conditions met in the controller routes.
Image for post
Image for post
Using substitution tags in the layout file to have a dynamic navigation bar.

Screen captures of the working application are provided below.

Image for post
Image for post
The main page for NatParkRev. Users are directed to login or sign up to see reviews.
Image for post
Image for post
Users are required to have an account in order to see reviews.
Image for post
Image for post
The main review page which shows all reviews in the database.
Image for post
Image for post
Users are capable of changing their account information.
Image for post
Image for post
An individual review page. If the logged-in user created the review, buttons are displayed to Edit and Delete.
Image for post
Image for post
Filtering reviews by the state of Washington.
Image for post
Image for post
“My Page” which shows all reviews that the logged-in user has made.
Image for post
Image for post
A user-friendly interface for creating reviews.

The Github repository for this application can be found here:
https://github.com/dougschallmoser/nat-park-rev-sinatra-app

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store