Learn something new every day: building a CRUD app with Sinatra

Theresa Morelli
9 min readAug 20, 2018
“A little boy holding a book with a surprised expression on his face” by Ben White on Unsplash

You learn something new every day, they say. For me this has been especially apparent lately as I venture into new (to me) coding territory with the Flatiron School. I reached my second of five portfolio projects, and it’s an exciting time for a nerd like me. The project is a chance to build something that’s totally me, to explore, to dive deeper.

I’ve been thinking a lot about time. The more it passes, the faster it seems to slip by. Summer is on the downswing already. Pumpkin spice is back in a week and sweater weather is most likely to follow. I wanted a simple, catalogable way to capture little bits of daily life that can so easily slip away. A CRUD app, the requirement for my second portfolio project in Sinatra with the Flatiron School, seemed the perfect medium. And so became my web app, Noted.

My design goals: 1) Encourage users to post at least one note per day, 2) Be easy to use and also versatile for different types of use. I decided to include tags for indexing or grouping of notes, as well as a public/private option.

I determined that four tables were needed: users, notes, tags, and the join table note_tags.

ERD showing my models and associations

The basic tables, models, and associations were relatively painless to set up. Feeling prepared by the lessons and labs on Learn.co, I got to work on the basic CRUD functionality working for notes routes. I hit a few snags including one with the patch and delete routes. I had mistakenly omitted the word “use” when loading Rack::MethodOverride (middleware that enables HTML put, patch, and delete requests) in my config.ru file. The line should read, use Rack::MethodOverride rather than just Rack::MethodOverride. Verbs make things happen, kids.

Bootstrap! It’s a front-end component library with built-in class styles. The lazy person’s answer to design. I briefly wondered if it’s wise to use from the start, as I could see myself losing focus on functionality and tinkering too much along the way, but I decided to add it because a nice view is a motivating force, and design thinking is kinda fun.

After getting the basic functions of the app in place, I did a ton of testing — logging in as different users, trying to view and edit different things, basically seeing what broke. This process helped me find and fix a bunch of problems, though it was a little haphazard. For now this works, but in the future I would like to be more systematic about finding and fixing errors using test-driven development.

And some screenshots of my submitted project:

View for adding a new note
View for user’s index page

Things I’m especially proud of

Successfully adding helper methods. The method names are descriptive phrases that describe what’s being checked for and the action taken. The logic is abstracted away, and reusing is as simple as calling a method where needed. Future refactors for this project will include more modularizing.

def redirect_to_login_if_not_logged_in(session)
if !logged_in?(session)
redirect to '/'
end
end
def redirect_to_new_note_or_index_if_logged_in(session)
if logged_in?(session)
# if a user's last note was created today, skip to index
# otherwise route to create note
if !current_user(session).notes.empty? &&
current_user(session).notes.last.created_at.localtime.to_date
== Time.now.to_date
redirect to '/index'
else
redirect to '/notes/new'
end
end
end
def redirect_to_index_if_unauthorized_to_view(session)
if @note.public == 0 && @note.user != current_user(session)
flash[:error] = "Hey, that's not your note"
redirect to '/index'
end
end
def redirect_to_index_if_unauthorized_to_edit_note(session)
if @note.user != current_user(session)
flash[:error] = "Hey, that's not your note"
redirect to '/index'
end
end
def redirect_to_index_if_unauthorized_to_edit_user(session)
if params[:id] != session[:user_id]
flash[:error] = "Hey, that's not your profile"
redirect to '/index'
end
end

Adding a favicon, the little picture in the browser tab. Making this work made me really happy and it was super easy to do. All I needed was a 16x16-pixel picture (PNG, GIF, or ICO) file and an extra line of code. I dropped the file in my project’s images folder, then within the <head> section of my main layout file, added the link tag below. The rel=“shortcut icon” indicates a favicon’s a comin, and the href points to its path. Voila!

<head>
<link rel="shortcut icon" href="/images/notepad.png"/>
</head>

As you may already know, it’s easy to find images on the internet, even a bunch that are already the right size. I took a slightly longer path and made one using https://logo.squarespace.com/ with tiny bit of editing to resize and to remove the watermark Squarespace adds.

Implementing tag edits for a note. Editing a note’s content is as straightforward as sending a patch request with the user-updated string. String replaces string. But tags are a little more complicated. When a user edits a note, it’s possible that tag objects may need to be added, deleted, or both.

I started by creating local variables to store both the edited tag array and the old tag array. To check if the user has removed any tags, I compared each old tag to the edited tags. If no match, the tag is deleted from that note’s tags. This results in the note and tag losing their association, though they both remain in the database. (Though keeping old tags around seemed like a good move at the time, it ended up causing some problems down the line in ‘Getting popular public tags.’)

The second part of editing a note’s tags is seeing if the user has added any tags. To do this I compared each edited tag with the old tags. If no match, either an existing tag is found or a new tag is created, and that tag is added to the current note’s tags.

edited_tags_array = params[:tags].split(",")
old_tags_array = old_note.tags.collect {|tag| tag.word}
old_note.tags.each do |tag|
if !edited_tags_array.include?(tag.word)
old_note.tags.delete(tag)
end
end
edited_tags_array.each do |tag|
if !old_tags_array.include?(tag)
old_note.tags << Tag.find_or_create_by(word: tag.downcase.strip)
end
end

Implementing user profile editing. I wanted a user to be able to edit their display name and username without changing their password. This was a little tricky because even without including password fields on the edit form, ActiveRecord was sending password params as an empty string on every submit, causing a validation error. Through googling, I figured out that this validation can be bypassed for a patch request by setting allow_nil: true in the model. This allows the edit form to be submitted with new name and/or username parameters while keeping the same password. A empty password on account creation still triggers a validation error.

class User < ActiveRecord::Base
validates :name, presence: true
validates :username, presence: true,
uniqueness: true
validates :password, presence: true,
confirmation: true,
length: { minimum: 1 },
allow_nil: true
validates :password_confirmation,
presence: true,
allow_nil: true
end

Getting popular public tags. If you’re allergic to unsightly code, now is a good time to look away. This one’s all about sticking with a difficult procedural challenge. My brain was sore after this one. My goal: a list tags from notes marked public sorted by most notes. Since I had foolishly chosen to allow orphaned tags to remain in the database, getting all the tags wouldn’t be as easy as Tag.all.

I started by collecting all tags from public notes in a flattened array. I grouped that array by the word value for each tag. This returned a hash with each key being the tag’s word and each value being an array of identical tags — the tags with that word that are currently associated with public notes. I then counted the tag objects in each array, sorted by that count, and reversed to get the tag list in descending order. I grabbed the first tag object in each array to form the @popular_public_tags array. Whew…

@popular_public_tags = Note.where(public: '1').collect{|note| note.tags}.flatten.group_by{|tag| tag.word}.sort_by{|k,v| v.size}.reverse.collect{|k,v| v[0]}

Is it clunky? Yes. Are there better ways to do it? Guaranteed. Did I stick with it and find a working solution. Absolutely. In retrospect I should have planned better. But getting this to work felt like solving a puzzle. And I like puzzles.

Displaying the correct date. Date is an integral property of a note, since it’s a key part of how information is organized. I included timestamps in my notes table, which automatically saves created_at and updated_at information. Though I was worried to discover that the information is stored in UTC time, a quick search revealed the Ruby method .localtime, which converts UTC time to local computer time. I couldn’t believe that conversion was so straightforward. I can foresee problems if a user travels between time zones, but for now it works great. Thanks Ruby! I then used the method .strftime to format the date. Sometimes programming feels like having access to a secret code.

@note.created_at
# => 2018-08-18 14:46:00 UTC
@note.created_at.localtime
# => 2018-08-16 10:46:00 -0400
# Yep, it's a 4-hour difference from US/Eastern where I am
@note.created_at.localtime.strftime("%A, %B %d, %Y")
# => "Saturday, August 18, 2018"
# Lovely!

Lessons I learned

Google first. Instead of fretting over a difficult-seeming problem, do a quick search. For this project, some of the problems I thought would be the worst to tackle, like time conversions, ended up being easy. Odds are someone else has had the same problem, and the solution is out there.

Know the rules well before breaking them. I wanted my app’s urls be short and descriptive and initially strayed from RESTful convention. This ended up being somewhat messy and realized I just didn’t know the rules well enough to eschew them. After converting to mostly RESTful routes, I too feel more restful.

Make a plan and stick to it. I realized that I have trouble focusing. I scroll around, indiscriminately making changes as I think of things to change. In the middle of working on a feature, I get sidetracked by a cool thing I could implement or a bug I notice. There are infinitely many things that can be done given infinite time. Unfortunately time is not infinite. Resist the urge. Stay focused with a clear vision for the future.

Improve commit discipline. Related to my focus issues. When I make a bunch of unrelated changes in one go, having to write a multi-line laundry list commit message is a recipe for confusion and disaster. This article https://alistapart.com/article/the-art-of-the-commit likens the commit message to a headline and the commit log to a newsfeed. The commit log tells a story about the history of the project. I love it. Thinking about a commit like a little milestone puts me in a more disciplined frame of mind, where I’m working on a focused task rather than just going through and making changes. In the future, I resolve to work on one task at a time, and to make a commit when that task is finished.

Future work to be done

New features:

  • Add a streak that keeps track of how many days in a row a user has posted.
  • Add an ideas button on the new note form to toggle a random prompt for inspiration.
  • Add calendar view.
  • Style tags pages with a word cloud.
  • Add delete user functionality.

Restructure user flow:

  • Simplify the index page. Show past notes and tags only if user chooses. Submitting a note redirects to a new ‘new note’ form. Make it straightforward to enter notes and with minimal distraction.
  • Break notes display into multiple pages if over 30 posts. Add view all option.
  • Display some pages differently based on the page the user is navigating from.
  • Truncate the link for long notes.

Refactoring:

  • Trash cleanup — tags with no associated notes get deleted, add delete user functionality.
  • Improve the logic for recent and popular tags methods (related to removing unused tags).
  • Move logic to controllers. Remove unnecessary code. Try to make code read like a sentence. Comment where necessary.

Summary

This project was great fun to build. I love the incremental gratification of acquiring a new language. The satisfying slow burn with occasional bursts. While communication is a weak point of mine and is something I’m always striving to improve, as I keep learning I’m feeling increasingly connected to the language of the web. Sometimes programming feels like solving a puzzle, sometimes it feels like having access to a secret code. Discovery lies around every corner along with vast reserves of possibilities, and I embrace it. Learn something new every day!

Github repo for Noted: https://github.com/theresamorelli/noted

--

--