Rails Project - Storyplan
Making a Rails application is hard. There are so many components, considerations, and features to be dealt with that the process can quickly become complicated and overwhelming, especially when you’re just starting out as a developer. For my first full-fledged Rails project, I’ll give an overview on how I broke down the process and attempted to build out the application one piece at a time.
The application is tentatively titled Storyplan and is intended as a tool to help writers plot out stories. Writing a story often requires a great deal of structure, planning, and constituent components, and I wanted to create a website that would make it easier to build and keep track of everything.
Models
I started by mapping out the models and migrations. Using the rails generate resource command, you can use Rails to generate a model, database migration for that model, and a controller, and I used it to get a head start. My models were as follows:
User- Generated using the Devise gem for authentication.Story- Belongs to aUser.Chapter- Belongs to aStory.Character- Has manyChaptersthrough a join table and vice versa, belongs to aUser.Audience- Has manyStoriesthrough a join table and vice versa.Genre- Has manyStoriesthrough a join table and vice versa.Comment- Belongs to both aUserand aStory.
With these models, I was able to set up the associations necessary to do things like show all the Stories of a User or all the Characters in a Story. This also makes it easier for a user to explore the different connections of a writer’s work.
These models have a variety of validations to ensure bad data isn’t entered into the system, as well as class and instance methods to give the models additional functionality beyond being mere data containers.
The Story model:
class Story < ActiveRecord::Base
belongs_to :user
has_many :story_genres
has_many :genres, -> { distinct }, through: :story_genres
has_many :story_audiences
has_many :audiences, through: :story_audiences
has_many :chapters, dependent: :destroy #destroys all chapters belonging to it when the story is destroyed
has_many :characters, through: :chapters
has_many :comments
validates :name, presence: true
validates :user_id, presence: true
validates :target_word_count, numericality: { only_integer: true }, allow_blank: true
validates :overview, length: { maximum: 4000 }
extend ClassOrderable
def recent_comments
self.comments.order('created_at DESC').last(10)
end
endRoutes and Controllers
Unlike Sinatra, Rails has a separate router from its controllers and uses it to map the routes to actions in those controllers. My routes were mostly resources for the above mentioned models, with a few nested resources to do things like the find all the Characters of a particular User.
As far as controllers are concerned, I used one controller per model with a few miscellaneous controllers thrown in as well to handle other tasks. This is basically a CRUD (Create, Read, Update, Delete) application, so many of the controllers handle similar tasks. So for example, the Stories controller handles serving all the stories for the stories index, showing a particular story, serving a form for a new story or editing an old one, processing the creation and update of a story based on input from the form, and destroying a story as well. These correspond to RESTful routes (Representational State Transfer): Index, Show, New, Create, Edit, Update, Destroy. This is simply a paradigm to keep things consistent across different web applications and is an example of Rails’s philosophy of convention over configuration.
There are other levels of complexity involved, such as authorization (making sure the user creating or updating a story has permission to do so) or white-listing the data received (using strong parameters) from the form so no improper data is forcibly entered.
See the story controller here.
Views
For the front end, I used the Bootstrap-Sass gem for Rails. It has a very generic look to it, but for now it works to help it look presentable.
The views use a variety of partials to make the display code reusable. For example, for each model, there is a partial that creates a sort of visual card for it that includes pertinent information. I then use a grid system to display collections of that model.
Code for the story card partial:
<% story = item unless story %>
<div class="col-sm-12 col-md-4">
<div class="thumbnail">
<div class="caption">
<h3><%= link_to story.name, story %></h3>
<p><strong>Written by:</strong> <%= link_to story.user.username, writer_path(story.user) %></p>
<p><strong>Overview: </strong><%= story_truncated_overview(story) %></p>
<p><strong>Target word count:</strong> <%= story.target_word_count %></p>
<% if !story.audiences.empty? %>
<p><strong>Target audience:</strong>
<% story.audiences.each do |audience| %>
<%= link_to audience.name, audience, class: "label label-warning" %>
<% end %>
</p>
<% end %>
<% if !story.genres.empty? %>
<p><strong>Genres:</strong>
<% story.genres.each do |genre| %>
<%= link_to genre.name, genre, class: "label label-primary" %>
<% end %>
</p>
<% end %>
</div>
</div>
</div>So when you go to the stories index page, it displays a grid of stories that simply repeat that model partial with new information from each collection item.
Authentication
Setting up a secure login/logout system can be challenging considering the number of security threats out there. You can roll your own authentication using bcrypt to securely store password hashes, but I wanted something with more functionality built in. That’s why I used the Devise gem which provides for the creation of its own User model and gives you additional tools like email confirmation should you desire them. Most importantly, it’s battle tested and commonly used to provide secure authentication for Rails applications.
In addition, I used Devise to provide OAuth authentication for Facebook, so you can securely sign in with a Facebook account as well.
Conclusion
Overall, this is just a small overview of what turned out to be a relatively complex project. There’s a lot more functionality I’d like to build eventually to make this more of a viable tool for writers, including building more of a front end, but for now it serves its purpose.
You can find the source code for this project here on Github.