CRUD, MVC and Sinatra
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.

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.

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.

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
.

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.

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.

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.

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.

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.

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.

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
.

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:
- Confirm that the user is logged in.
- Create a new instance of
Post
by passing in the params hash with the key of:post
. - Assign and associate the new post’s user to the current user.
- Save the newly created post and redirect if the post successfully saves.

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:
- Confirm that the user is logged in.
- 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. - Confirm that the current user is associated as the owner of the post.
- Render the
posts/edit
view.

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.

The associated patch '/posts/:id
dynamic route is called to action which will:
- Confirm that the user is logged in.
- 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. - Confirm that the current user is associated as the owner of the post.
- Redirect to the
/posts/:id
route if the updated post saves in the database.

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:
- Confirm that the user is logged in.
- 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 theUser.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). - Render the
/users/show
page only if the user is found and the user’s username is equivalent to the session’s username.

The post '/login'
route utilizes the #authenticate
method and will:
- Find the
User
instance by searching the database for username that the user entered in the form. - Confirm that the user instance exists and that the password entered into the form matches the password in the database.
- Set the
:username
key of the session hash to that particular user’susername
. - Redirect to the previously sought after URL if one was set, or redirect to the user’s page if conditions are met.

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.


Screen captures of the working application are provided below.








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