Sinatra: Helpful Tips for Modular Apps

Hannah Reitzel Rivera
9 min readMar 8, 2021

--

I set out to write a gradebook app teachers could use to manage students, course information, and of course, grades. A gradebook is a relatively simple item, I thought. Each course is essentially a table of students and assignments, at the intersection of which is their grades.

First Level Complexity: Models and Relationships

To accurately represent a gradebook, I decided four model classes were necessary: Teacher, Student, Course, and Assignment. These models all inherit from ActiveRecord::Base, since I have used ActiveRecord to manage my database.

Relationships Among Models

I was a teacher before I began coding, so I used that knowledge to structure my models. The simplest, Teacher, would have many courses, and through those, many students.

class Teacher < ActiveRecord::Base
has_many :courses
has_many :students, through: :courses
end

Assignment was also relatively simple to write and relate to other classes:

class Assignment < ActiveRecord::Base
belongs_to :course
has_many :students
end

As written, assignments belong to courses, meaning in the database the ‘assignments’ table has a foreign key of course_id, a great giveaway that something is in a ‘belongs_to’ relationship.

I next began to write Course. Each course belongs to a Teacher, and has many Assignments and Students. Simple!

class Course < ActiveRecord::Base
belongs_to :teacher
has_many :assignments
has_many :students
end

The Student class was where I started to realize nothing about this was actually simple. Students don’t belong to anything, really, but they do have a lot! They have many courses, teachers, and assignments. Teachers I could simplify using the same “through” relationship I had used in the Teacher model, but what of courses and assignments?

class Student < ActiveRecord::Base
has_many :courses
has_many :teachers, through: :courses
has_many :assignments
end

There’s nothing wrong with has_many relationships. They’re very useful. The difficulty occurs when an object of Class A has many objects of Class B, and each object in Class B also has many objects of Class A. This is a special and more complicated case known as a has_many, has_many relationship. It calls for the creation of a join table in the database and a joined model class. Here is the migration I used to create the join table for Student and Course:

class CreateStudentCourses < ActiveRecord::Migration[6.0]
def change
create_table :student_courses do |t|
t.integer :student_id
t.integer :course_id
end
end
end

Seeing a table like this, which only contains foreign keys, tells a programmer that they are looking at a join table. The naming convention is also helpful in showing a join table. The spot where I was MOST lost in creating my database was where to put the actual grades. This is a gradebook! But what do grades belong to? Assignments? Students? Courses? I decided that the best expression of a grade’s truest nature is at the join of Student and Assignment. Therefore, with trepidation (because I had never seen a join table with something other than foreign keys), I wrote the following join table:

class CreateStudentAssignments < ActiveRecord::Migration[6.0]
def change
create_table :student_assignments do |t|
t.integer :student_id
t.integer :assignment_id
t.string :grade
end
end
end

Having written my basic tables (not shown here) and my two join tables, I went back and edited my four basic models, as well as adding models for each of the join tables. Models are needed for join tables because the relationships they express are unique objects, which is well-illustrated by the fact that a grade really only properly exists at the intersection of a student and an assignment. Here is an example of the Student model class after adding in the join tables and the additional relationships they represent:

class Student < ActiveRecord::Base
has_many :student_courses
has_many :courses, through: :student_courses
has_many :teachers, through: :courses
has_many :student_assignments
has_many :assignments, through: :student_assignments
end

Second Level Complexity: Controllers and Views

After creating my database relationships, completing migrations, and seeding the database, it was time to make the data visible and usable on the web. I used the basic CRUD and RESTful routing techniques I have learned to build routes into each of four controllers: ApplicationController, CourseController, StudentController, and AssignmentController.

A table showing examples of RESTful routing for a photo website.
RESTful routing example from this blog. There are differences in how PATCH and PUT update a record.

ApplicationController handles user signup and login, as well as root routing. CourseController handles all routes that have to do with a teacher user’s courses, including the Gradebook view rendered by show.erb for each Course. StudentController and AssignmentController respectively handle all requests for CRUD on their model classes.

class AssignmentController < ApplicationController  get '/courses/:id/assignments' do
@course = Course.find_by(id: params[:id])
redirect to "/courses/#{@course.id}"
end
get '/courses/:id/assignments/new' do
@course = Course.find_by(id: params[:id])
erb :'assignments/new'
end
get '/courses/:course_id/assignments/:id' do
@course = Course.find_by(id: params[:course_id])
@assignment = Assignment.find_by(id: params[:id])
erb :'assignments/show'
end
get '/courses/:course_id/assignments/:id/edit' do
@course = Course.find_by(id: params[:course_id])
@assignment = Assignment.find_by(id: params[:id])
erb :'assignments/edit'
end
post '/courses/:id/assignments' do
@course = Course.find_by(id: params[:assignment][:course_id])
if !params[:assignment][:name].empty?
@assignment = Assignment.create(params[:assignment])
@assignment.course = @course
redirect to "/courses/#{@course.id}"
else
flash[:message] = "ASSIGNMENT NOT CREATED. Name is required."
redirect to "/courses/#{@course.id}"
end
end
...
end

In the snippet of AssignmentController above, most of the RESTful routes are visible. To maintain DRY code and the single responsibility principle/separation of concerns, I tried to ensure that routes were a maximum of ten lines. The exception tends to be the PATCH routes, which tend to run around fifteen lines of code since they require more control flow than other routes (to properly update objects).

With the exception of the teacher model that uses main views (allowing for signup, login, and user account editing), each model corresponds to a set of views: edit, new, and show. Courses and students also have index pages, but assignments do not as they are nested within courses. The index page for all assignments in a course is the same as the show page for that course — the location of the gradebook.

Screenshot of gradebook view for a course called Math 2, showing several students and their grades for several assignments. On the right is the course navigation bar.
Gradebook for the Math 2 Course, showing five students each with six assignments and their respective grades. The assignment and student names are each linked to a route that renders a show page.

Behind the scenes is the view code for the gradebook:

<div class="content">
<% if AuthenticationHelper.logged_in?(session) && AuthenticationHelper.current_user(session).id == @course.teacher.id %>
<h2><%= @course.name %></h2>
<p>Click any assignment name to edit the assignment or its grades.</p>
<p>To add a new assignment or edit the coures, please use the buttons below the gradebook.</p>
<table border="3">
<thead>
<th>Student</th>
<% if @course.assignments %>
<% @course.assignments.each do |assignment| %>
<th><a href='/courses/<%= @course.id %>/assignments/<%= assignment.id %>'><%= assignment.name %></a></th>
<% end %>
<% end %>
</thead>
<% if @course.students %>
<% @course.students.each do |student| %>
<tr>
<td><a href='/students/<%= student.id %>'><%= student.name %></a></td>
<% student.student_assignments.joins(:assignment).where("assignments.course_id = ?", @course.id).order(:assignment_id).each do |sa| %>
<td><%= sa.grade ||= "" %></td>
<% end %>
</tr>
<% end %>
<% end %>
</table><br>
<a href='/courses/<%= @course.id %>/assignments/new'><button>Add Assignment</button></a>
<a href='/courses/<%= @course.id %>/edit'><button>Edit Course</button></a><br><br>
<form action='/courses/<%= @course.id %>' method="POST">
<input type="hidden" name="_method" value="DELETE">
<input type="submit" value="Delete Course">
</form><br>
<% else %>
<p>ERROR: You must be logged in and own this course to view this page.</p>
<% end %>
</div>

I chose to show this because it is the most complex view I wrote, and even then I would not say it has much logic in it. At the top it uses helper methods from a class I built called ApplicationHelper to ensure that a user is logged in and owns the course before the teacher can view the gradebook. Most of my views have basic verification/authentication of login status and current user identification.

Third Level Complexity: Validation and Security

Using ActiveRecord and the bcrypt gem, I updated my Teacher class to have a secure password, and then concomitantly updated the teachers table in the database to contain a password_digest instead of just a password. I also realized that I did not want teachers to be able to sign up for accounts without a unique email address or without filling in all fields of the signup form.

class Teacher < ActiveRecord::Base
has_many :courses
has_many :students, through: :courses
validates :first_name, :last_name, :email, :password, presence: true
validates :email, uniqueness: true
has_secure_password
end

I added basic name validation to both assignments and courses as well. In my routes, I added logic to check the session, and on each view page that requires a login, I used helper functions to ensure that the user had the proper authentication to view the page.

Next Level Complexity: Current Questions and Directions for Future Work

Currently, my application is functional and does everything a gradebook ought to do in terms of storing grades, students, and courses, and being able to perform CRUD operations on all of them. I have tried to break it in various ways, as well as attempting to directly access pages that require login, and have not so far been able to crack through the basic validation I have set.

One odd problem plagued me throughout application development, and that is an issue with calling multiple controllers. For some reason, despite routes being correct, my application does not seem to be able to call the routes in StudentController and CourseController if I have both staged as middleware in the config.ru file as shown here:

# in config.ru
...
use Rack::MethodOverride
use CourseController
use StudentController
use AssignmentController
run ApplicationController

If I leave this configuration in place, I can run routes that begin with ‘/courses’ but not routes that begin with ‘/students’. If I switch their places, the opposite occurs. Routes in ApplicationController work fine (all of its routes work off of the root directory ‘/’) and routes in AssignmentController are also fully functional (they all stem from ‘/courses/:course_id/assignments’). In the end, in order to have both controllers functional, I had to use an inelegant solution:

# in config.ru
...
use Rack::MethodOverride
map('/students') { run StudentController }
use CourseController
use AssignmentController
run ApplicationController

Before I implemented this solution, I would get redirected to ‘/’ when attempting to access ‘/students’ and a status code of 302 for ‘moved temporarily.’ I researched extensively how to fix this error elegantly, but so far have not found a solution.

Besides this problem, my goals for ‘next steps’ for this little app are:

  • Add functionality for averaging and displaying the average of a student’s grades in the Course (gradebook) and Student show views
  • Add functionality for averaging and displaying the average of an assignment’s grades in the Course (gradebook) and Assignment show views
  • Add styling and overall slicker look and feel to the app by utilizing Bootstrap or similar

Overall, this was a challenging and enjoyable coding project that could actually be useful. I am excited to continue improving it as I increase my coding repertoire and add to my portfolio. To summarize what I learned, the following tips/tricks were most helpful to me in writing this project:

  1. Use a gem like Corneal to create the project structure. For a Sinatra modular app with MVC structure, it was great. I had very little work to do in setting up the project structure after it had created its defaults.
  2. Initialize a GitHub repository for the project and get its remote setup right away. Make very frequent commits with meaningful messages. I failed to do this and it is the coding practice I am now most trying to improve.
  3. Draw or write out the models and their relationships before coding the migrations for tables. Having a strong grasp of how data will be related is really helpful in creating the tables.
  4. Write the models and migrations first so the database can be seeded before working on the frontend of the app. I chose to simply write some seed data in a ‘seeds.rb’ file, but a gem like Faker can also be used to generate data depending on app function.
  5. When it’s time to write the frontend of the app — the controllers and views — start with the main ApplicationController. Get signup, login, and logout working before anything else. They can be some of the hardest routes to write and it’s worth getting them right before proceeding.
  6. Once basic user functions are up and running, use a RESTful template to write the routes in other controllers. I found it helpful to simply write stubs of all of the desired routes in order before filling them out. This helped me avoid routing order problems.
  7. Create the following views for each controller: index, new, show, and edit. These views ensure coding with CRUD in mind. The basic views may be altered as coding proceeds, but they create a consistent foundation.
  8. Minimize logic in views. While some is necessary for authentication and iteration, the less logic that takes place in the view, the cleaner it is and the less vulnerable to tampering.
  9. If more logic seems to be needed in a view, it probably belongs in a model. For instance, I realized that when a new student was added to a course, its existing assignments would need to be added to the ‘.assignments’ attribute of the student, so I wrote a method in the StudentCourse class.
  10. When debugging routing, just put simple HTML in the route to check if it’s being accessed. It never hurt anyone to try a “Hello World” when things aren’t working right.
  11. Pay attention to the output of the terminal where the server is running. I used Shotgun so I could save changes and see them without restarting the server. The terminal shows useful information like the route the browser is requesting and the status code. This is how I discovered that my get route for ‘/students’ was returning a 302 and redirecting to root.
  12. Write helper methods. I noticed while adding user authentication that I was having to check the session status for a user being logged in, as well as getting the current user id frequently, so I wrote these methods to a class and then called them wherever needed in views.

--

--

Hannah Reitzel Rivera

Former archaeologist and high school teacher turned software engineer. Just trying to learn and solve puzzles!