Houseplant Helper is a web app built with Rails that helps Users
track how recently they have watered their Plants
and lets them know when a Plant
needs to be watered.
Project Architecture
Houseplant Helper employs “MVC” (Model, View, Controller) architecture that is convention in Rails. This architecture defines clear boundaries to handle model logic, presentation, and HTTP request handling.
Models
Models are connected to the database with ActiveRecord::Base
and take advantage of Active Record Lifecycle Callbacks – specifically the Plant
class which invokes before_validation
and before_save
.
Users
and Species
are joined by the Plants
table which also has custom attributes of nickname
and water_frequency
. If water_frequency
is nil
, the Plant
will match the water_frequency
of the Species
before it is saved to the database.
Partials
Houseplant Helper takes advantage of Rails partials to DRY up its views. Since all views share the errors
partial, it is located inside the shared
directory. Most views also take advantage of rendering a shared form
partial which uses form_with
to accept one or more instance variables.
Since Houseplant Helper takes advantage of nested routes, forms rely on a url
to be passed in through an array. If the value of the instance variable is nil
(it was never created in the controller), Rails will remove the variable.
The new_user_plant_path
(/users/:user_id/plants/new) relies on a @user
, while edit_plant_path
(/plants/:id/edit) relies on a @plant
. Since @user
is not created in the edit_plant_path
, Rails will remove the nil
value, use the @plant
variable, and create a <form>
with the PATCH
method as a hidden input.
<%= form_with(model: @plant, url: [@user, @plant]) do |f| %>
<div class="form-section application-form">
<%= f.label :nickname %>
<%= f.text_field :nickname %>
</div>
<div class="form-section application-form">
<%= f.label :species_id %>
<%= f.collection_select :species_id, Species.all, :id, :common_name, prompt: true %>
</div>
<div class="form-section application-form">
<%= f.label :water_frequency, "Number of days between watering your plant" %>
<%= f.number_field :water_frequency, step: 1 %>
</div>
<%= f.submit id: "application-button"%>
<% end %>
Houseplant Helper also uses the plant
partial across several views which accepts a collection of @plants
and renders the partial for every item in the collection. With some additional logic from the helpers, this partial is used in the user_plants_path
and species_path
, and will display different buttons if the Plant
belongs to the logged in User
.
Plant partial rendered in the user_plants_path
Plant partial rendered in the species_path
Helpers and Controllers
Since all controller inherit from ApplicationController
, ApplicationController
calls a before_action
of a private method redirect_if_not_logged_in
. The HomeController
is uses skip_before_action
to allow users to create sessions. Similarly, the UsersController
uses skip_before_action
on new
and create
.
ApplicationController
uses helper_method
to make two of its private methods available to in the views – current_user
and logged_in?
. To avoid unnecessary queries, the current_user
method uses or-equals to rely on the same instance variable.
def current_user
@current_user ||= User.find_by_id(session[:user_id])
end
Adhering to RESTful Convention
The plants#index
controller action will look to the URI for query parameters and filter the @plants
collection for the @user
in the user_plants_path
(/users/:user_id/plants).
def find_and_set_plants_and_filter_query_parameters(request)
if !request.query_parameters.empty?
request.query_parameters.each do |scope, value|
@plants ||= @user.plants
@plants = @plants.select do |plant|
plant.send("#{scope}").to_s == value
end
end
else
@plants = @user.plants
end
end
A user can quickly apply these query parameters by clicking ‘Show Overdue’ at the top of the page.
The collection can be queried with any instance method and adhering to query parameter restful convention
Third Party Authentication
Users can also create an account and log in through Google thanks to the omniauth
gem. The User model has a class method from_omniauth
that uses find_or_create_by
to find a User with a uid
that matches the `request.env[‘omniauth.auth’] returned from Google.
Resources
Feel free to check out Houseplant Helper on my Github or give me a follow on Twitter to continue following my coding journey.
Houseplant Helper is licensed with a BSD 2-Clause License.