I Built a Sinatra App for Landlords to list and manage their properties.

For our second FlatIron project, we were tasked to build a CRUD, MVC app using Sinatra. It should be a custom app that we create to track something that is important to us.

I decided to build an app for Landlords because I have been a tenant for the past 4 years and have had close relationships with my Landlords. I was always interested in what they did so I decided to make an app that would help landlords manage their properties.

Pre-requisites

My application utilizes these basic concepts:

  • Basic Ruby concepts

  • Object-oriented programming in Ruby

  • CRUD: create, read, update, and delete. Four basic functions of persistent storage.

  • MVC: model-view-controller software design pattern.

  • Basic HTML and CSS

Planning

My first step to planning out this project was building a checklist from my Roam database.

Screen Shot 2020-05-17 at 8.16.44 PM.png

For my first project, I used a kanban board on Notion. For this project, I decided to use a basic checklist. After utilizing both, I think I prefer using the kanban board as a project manager. It's a lot more visual which I really like.

Before writing down any code I created my association declaration and a visual representation of my models.

Association declarations enable models in your program to have relationships.

class User < ActiveRecord
    has_many :properties
    has_many :tenants, through: :properties
end 

class Property < ActiveRecord 
    belongs_to :user
    has_one :tenant 
end 

class Tenant < Active Record 
    belongs_to :property 
end

In this declaration, we can see that User/Landlord "has many" properties and "has many" tenants through its properties. The Property class "belongs to" a landlord and "has one" tenant. The Tenant class "belongs to" to a property.

To better describe my representations, I create a visual model.

aN8EOmqi5V.png

With the Visual model, it's much simpler to see how these relationships are represented.

Allocating Responsibilities

In an Object-Oriented application, it's important to distribute the tasks to different models and controllers. Based on my observation, I created these three models:

  • A User class, responsible for creating each instance of a user/landlord. It's also responsible for creating a relationship with the property class.
class User < ActiveRecord::Base 
    has_secure_password
    has_many :properties
    has_many :tenants, through: :properties
end
  • A Property Class, responsible for creating each instance of a property. It's also responsible for knowing that it belongs to a landlord and that it has one tenant.
class Property < ActiveRecord::Base 
    belongs_to :user
    has_one :tenant
end
  • A Tenant class, responsible for creating each instance of a tenant. It's also responsible for knowing it belongs to a property.
class Tenant < ActiveRecord::Base 
    belongs_to :property  
end

The controller class inherits from the Application Controller and has methods just like any other class. The controller differs in a regular class in that it responds to requests from the router.

So in all the controller will receive a request, fetch or save data from a model, and use a view to create HTML output. The controller can be looked at as the middleman between the model and views. It makes the model data available to the view so it can display that data to the user, and it saves or updates user data to the model.

  • Application controller, responsible for rendering the welcome view, holds the helper methods which all the other controllers can use.
require './config/environment'

class ApplicationController < Sinatra::Base

  configure do
    set :public_folder, 'public'
    set :views, 'app/views'
    enable :sessions
    set :session_secret, "password_security"
  end

  get "/" do
    @logged_in = logged_in?
    erb :welcome
  end

  helpers do
    def logged_in? 
      !!session[:user_id]
    end 

    def current_user
      User.find(session[:user_id])
    end 
  end

end
  • User controller, responsible for rendering users home page, the signup form, login form, and logout requests.
class UsersController < ApplicationController 

    get '/users/home' do
        if logged_in? 
            erb :"users/index"
        else
            redirect '/login'
        end 
    end 

    get '/signup' do
        if logged_in?
            redirect '/properties'
        end 
        erb :"users/create_user"
    end

    post '/signup' do
        unless params[:name] == "" || params[:email] == ""
            user = User.new(name: params[:name], email: params[:email], password: params[:password])
            if user.save 
                session[:user_id] = user.id 
                redirect '/properties'
            end 
        end
        redirect '/signup'
    end

    get '/login' do
        unless logged_in?
            erb :'users/login'
        else
            redirect '/users/home'
        end 
    end

    post '/login' do
        user = User.find_by(email: params[:email])
        if user && user.authenticate(params[:password])
            session[:user_id] = user.id 
            redirect '/users/home'
        else
            redirect '/login'
        end 
    end

    get '/logout' do
        if logged_in?
            session.clear
        end 
        redirect '/login'
    end

    post '/logout' do
        session.clear
        redirect '/login'
    end

end
  • Property controller, responsible for rendering the users properties, new properties form, processing the new property form, rendering a specific property, updating a specific property, and deleting a specific property.
class PropertiesController < ApplicationController 

    get '/properties' do
        if logged_in?
            @first_name = current_user.name.split[0]
            @properties = current_user.properties
            erb :"/properties/index"
        else
            redirect '/login'
        end 
    end

    get '/properties/new' do
        if logged_in?
            @tenants = Tenant.all.select { |x| x.property_id == nil}
            erb :"/properties/new"
        else
            redirect '/login'
        end 
    end

    post '/properties' do
        @property = current_user.properties.build(address: params[:property][:address], description: params[:property][:description], rent: params[:property][:rent])
        if !@property.address.empty? && !@property.description.empty? && !@property.rent.empty?
            @property.save
            if params[:tenant]
                @property.tenant = Tenant.find_by_id(params[:tenant][:tenant_id][0].to_i)
            end 
            redirect '/properties'
        else 
            redirect '/properties/new'
        end 
    end

    get '/properties/:id' do
        if logged_in?
            @property = current_user.properties.find_by_id(params[:id])
            erb :"properties/show"
        else 
            redirect '/login'
        end 
    end 

    get '/properties/:id/edit' do
        if logged_in? 
            @property = current_user.properties.find_by_id(params[:id])
            @tenants = Tenant.all.select { |x| x.property_id == nil}
            erb :"properties/edit"
        else 
            redirect
        end 
    end 

    patch '/properties/:id' do
        @property = current_user.properties.find_by_id(params[:id])
        @property.update(params[:property])
        if params[:tenant]
            @property.tenant = Tenant.find_by_id(params[:tenant][:tenant_id][0].to_i)
            redirect "/properties/#{@property.id}"
        else
            redirect "/properties/#{@property.id}"
        end
    end

    delete '/properties/:id' do
        @property = current_user.properties.find_by_id(params[:id])
        @property.tenant = nil  
        @property.destroy 
        redirect '/properties'
    end 
end
  • Tenant controller didn't have any responsibilities because I wanted to simulate that there are already tenants on the market so I created some seed data.
class TenantsController < ApplicationController 

end

Conclusion

This project was honestly a lot of fun to build. I really felt like FlatIron prepared us enough to do well on this project. There were times where I felt stuck or lost but using methods such as binding.pry helped a lot.

Fork/clone the project from my repo and give it a go!

Happy coding!