Singing with Sinatra – The Recall App

Singing with Sinatra – The Recall App

Tutorial Details
  • Topic: Sinatra / Ruby
  • Difficulty: Beginner
This entry is part 2 of 3 in the Singing with Sinatra Session
« PreviousNext »

Welcome to Track 2 of Singing with Sinatra. In part one, we reviewed Routes, how to work with URI parameters, working with forms, and how Sinatra differentiates routes by the HTTP method they were requested by. Today, we’re going to extend our knowledge of Sinatra by building a small database-driven app, “Recall,” for taking notes/making a to-do list.

We’re going to be using a SQLite database to store the notes, and we’ll use the DataMapper RubyGem to communicate with the database. Run the following inside a shell to install the relevant Gems:

gem install sqlite3 datamapper dm-sqlite-adapter

Depending on how you have RubyGems set up on your system, you may need to prefix gem install with sudo.


The Warm-Up

Let’s jump right in by creating a new directory for the project, and creating the application file, recall.rb. Start it off by requiring the relevant gems:

require 'rubygems'
require 'sinatra'
require 'datamapper'

Note: If you’re running Ruby 1.9 (which you should be), you can drop the “require ‘rubygems’” line as Ruby automatically loads RubyGems anyway.

And set up the database with the following:

DataMapper::setup(:default, "sqlite3://#{Dir.pwd}/recall.db")

class Note
  include DataMapper::Resource
  property :id, Serial
  property :content, Text, :required => true
  property :complete, Boolean, :required => true, :default => false
  property :created_at, DateTime
  property :updated_at, DateTime
end

DataMapper.finalize.auto_upgrade!

On the first line we’re setting up a new SQLite3 database in the current directory, named recall.db. Below that, we’re actually setting up a ‘Notes’ table in the database.

While we’re calling the class ‘Note’, DataMapper will create the table as ‘Notes’. This is in keeping with a convention which Ruby on Rails and other frameworks and ORM modules follow.

Inside the class, we’re setting up the database schema. The ‘Notes’ table will have 5 fields. An id field which will be an integer primary key and auto-incrementing (this is what ‘Serial’ means). A content field containing text, a boolean complete field and two datetime fields, created_at and updated_at.

The very last line instructs DataMapper to automatically update the database to contain the tables and fields we have set, and to do so again if we make any changes to the schema.


The Home Page

Now, let’s create our home page:

At the top is a form to add a new note, and below it is all the notes in the database. To get started, add the following to the application file, recall.rb:

get '/' do
  @notes = Note.all :.order => :id.desc
  @title = 'All Notes'
  erb :home
end

Important Note: Remove the dot (‘.‘) in :.order. (WordPress is interfering with the code sample.)

On the second line you see how we retrieve all the notes from the database. If you’ve used ActiveRecord (the ORM used in Rails) before, DataMapper’s syntax will feel very familiar. The notes are assigned to the @notes instance variable. It’s important to use instance variables (that’s variables beginning with an @) so that they’ll be accessible from within the view file.

We set the @title instance variable, and load the views/home.erb view file through the ERB parser.

Create the views/home.erb view file and start it off with the following:

<section id="add">
  <form action="/" method="post">
    <textarea name="content" placeholder="Your note&hellip;"></textarea>
    <input type="submit" value="Take Note!">
  </form>
</section>

<% # display notes %>

We have a simple form which POSTs to the home page ('/'), and below that is some ERB code serving as a placeholder for now.


Layouts

The HTML standards lot amongst you may have suffered a minor stroke after seeing that our home view file contains no doctype or other HTML tags. Well, there’s a reason for that. Create a layout.erb file in your views/ directory containing the following:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf8">
  <title><%= @title + ' | Recall' %></title>
  <link href="/reset.css" rel="stylesheet">
  <link href="/style.css" rel="stylesheet">
</head>
<body>
  <header>
    <hgroup>
      <h1><a href="/">Recall</a></h1>
      <h2>'cause you're too busy to remember</h2>
    </hgroup>
  </header>

  <div id="main">
    <%= yield %>
  </div>

  <footer>
    <p><small>An app for <a href="http://net.tutsplus.com">Nettuts+</a>.</small></p>
  </footer>
</body>
</html>

The two interesting parts here are lines 5 and 18. On line 5 you see the first use of the <%= … %> ERB tags. <%= is different from the ordinary <% as it prints what is inside. So here we’re displaying the whatever’s in the @title instance variable followed by | Recall for the page’s <title> tag.

On line 18 is <%= yield %>. Sinatra will display this layout.erb file on all Routes. And the actual content for that route will be inserted wherever the yield is. yield is a term which essentially means “stop here, insert whatever’s waiting, then continue on”.

Start up the server with shotgun recall.rb in the shell, and take a look at the home page in the browser. You should see content from the layout file, and the form from the actual home.erb view.


CSS

In the layout file we included two CSS files. Sinatra can load static files (eg. your CSS, JS, images etc.) from a folder named public/ in the root directory. So create that directory, and inside it two files: reset.css and style.css. The reset contains the HTML5 Boilerplate CSS reset:

/*
	HTML5 ✰ Boilerplate

	style.css contains a reset, font normalization and some base styles.

	credit is left where credit is due.
	much inspiration was taken from these projects:
		yui.yahooapis.com/2.8.1/build/base/base.css
		camendesign.com/design/
		praegnanz.de/weblog/htmlcssjs-kickstart
*/

/*
	html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline)
	v1.6.1 2010-09-17 | Authors: Eric Meyer & Richard Clark
	html5doctor.com/html-5-reset-stylesheet/
*/

html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
	margin:0;
	padding:0;
	border:0;
	font-size:100%;
	font: inherit;
	vertical-align:baseline;
}

article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
		display:block;
}

blockquote, q { quotes:none; }

blockquote:before, blockquote:after,
q:before, q:after { content:''; content:none; }

ins { background-color:#ff9; color:#000; text-decoration:none; }

mark { background-color:#ff9; color:#000; font-style:italic; font-weight:bold; }

del { text-decoration: line-through; }

abbr[title], dfn[title] { border-bottom:1px dotted; cursor:help; }

table { border-collapse:collapse; border-spacing:0; }

hr { display:block; height:1px; border:0; border-top:1px solid #ccc; margin:1em 0; padding:0; }

input, select { vertical-align:middle; }

/* END RESET CSS */

/* font normalization inspired by  from the YUI Library's fonts.css: developer.yahoo.com/yui/ */
body { font:13px/1.231 sans-serif; *font-size:small; } /* hack retained to preserve specificity */
select, input, textarea, button { font:99% sans-serif; }

/* normalize monospace sizing
 * en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */
pre, code, kbd, samp { font-family: monospace, sans-serif; }

/*
 * minimal base styles
 */

body, select, input, textarea {
	/* #444 looks better than black: twitter.com/H_FJ/statuses/11800719859 */
	color: #444;
	/* set your base font here, to apply evenly */
	/* font-family: Georgia, serif;  */
}

/* headers (h1,h2,etc) have no default font-size or margin. define those yourself. */
h1,h2,h3,h4,h5,h6 { font-weight: bold; }

/* always force a scrollbar in non-IE: */
html { overflow-y: scroll; }

/* accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */
a:hover, a:active { outline: none; }

a, a:active, a:visited { color: #607890; }
a:hover { color: #036; }

ul, ol { margin-left: 2em; }
ol { list-style-type: decimal; }

/* remove margins for navigation lists */
nav ul, nav li { margin: 0; list-style:none; list-style-image: none; }

small { font-size: 85%; }
strong, th { font-weight: bold; }

td { vertical-align: top; }

/* set sub, sup without affecting line-height: gist.github.com/413930 */
sub, sup { font-size: 75%; line-height: 0; position: relative; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }

pre {
	/* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */
	white-space: pre; white-space: pre-wrap; white-space: pre-line; word-wrap: break-word;
	padding: 15px;
}

textarea { overflow: auto; } /* www.sitepoint.com/blogs/2010/08/20/ie-remove-textarea-scrollbars/ */

.ie6 legend, .ie7 legend { margin-left: -7px; } /* thnx ivannikolic! */

/* align checkboxes, radios, text inputs with their label by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css  */
input[type="radio"] { vertical-align: text-bottom; }
input[type="checkbox"] { vertical-align: bottom; }
.ie7 input[type="checkbox"] { vertical-align: baseline; }
.ie6 input { vertical-align: text-bottom; }

/* hand cursor on clickable input elements */
label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; }

/* webkit browsers add a 2px margin outside the chrome of form elements */
button, input, select, textarea { margin: 0; }

/* colors for form validity */
input:valid, textarea:valid   {  }
input:invalid, textarea:invalid {
			border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red;
}
.no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; }

/* These selection declarations have to be separate.
	 No text-shadow: twitter.com/miketaylr/status/12228805301
	 Also: hot pink. */
::-moz-selection{ background: #FF5E99; color:#fff; text-shadow: none; }
::selection { background:#FF5E99; color:#fff; text-shadow: none; }

/*  j.mp/webkit-tap-highlight-color */
a:link { -webkit-tap-highlight-color: #FF5E99; }

/* make buttons play nice in IE:
	 www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */
button {  width: auto; overflow: visible; }

/* bicubic resizing for non-native sized IMG:
	 code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */
.ie7 img { -ms-interpolation-mode: bicubic; }

And style.css contains some basic styling to make the app look pretty:

body {
	margin: 35px auto;
	width: 640px;
}

header {
	text-align: center;
	margin: 0 0 20px;
}

header h1 {
	display: inline;
	font-size: 32px;
}

header h1 a:link, header h1 a:visited {
	color: #444;
	text-decoration: none;
}

header h2 {
	font-size: 16px;
	font-style: italic;
	color: #999;
}

#main {
	margin: 0 0 20px;
}

#add {
	margin: 0 0 20px;
}

#add textarea {
	height: 30px;
	width: 510px;
	padding: 10px;
	border: 1px solid #ddd;
}

#add input {
	height: 50px;
	width: 100px;
	margin: -50px 0 0;
	border: 1px solid #ddd;
	background: white;
}

#edit textarea {
	height: 30px;
	width: 480px;
	padding: 10px;
	border: 1px solid #ddd;
}

#edit input[type=submit] {
	height: 50px;
	width: 100px;
	margin: -50px 0 0;
	border: 1px solid #ddd;
	background: white;
}

#edit input[type=checkbox] {
	height: 50px;
	width: 20px;
}

article {
	border: 1px solid #eee;
	border-top: none;
	padding: 15px 10px;
}

article:first-of-type {
	border: 1px solid #eee;
}

article:nth-child(even) {
	background: #fafafa;
}

article.complete {
	background: #fedae3;
}

article span {
	font-size: 0.8em;
}

p {
	margin: 0 0 5px;
}

.meta {
	font-size: 0.8em;
	font-style: italic;
	color: #888;
}

.links {
	font-size: 1.8em;
	line-height: 0.8em;
	float: right;
	margin: -10px 0 0;
}

.links a {
	display: block;
	text-decoration: none;
}

Refresh the page in your browser and everything should be more styled. Don’t worry about this CSS too much; it just makes things look a bit prettier!


Adding a Note to the Database

Right now if you try submitting the form on the home page, you’re going to get a route error. Let’s create the POST route for the home page now:

post '/' do
  n = Note.new
  n.content = params[:content]
  n.created_at = Time.now
  n.updated_at = Time.now
  n.save
  redirect '/'
end

So when a post request is made on the homepage, we create a new Note object in n (thanks to the DataMapper ORM, Note.new represents a new row in the notes table in the database). The content field is set to the submitted data from the textarea and the created_at and updated_at datetime fields are set to the current timestamp.

The new note is then saved, and the user redirected back to the homepage where the new note will be displayed.


Displaying the Notes

So we’ve added a new note, but we can’t see it on the homepage yet as we haven’t wrote the code for it. Inside the views/home.erb view file, replace the <%# display notes %> line with:

<% @notes.each do |note| %>
  <article <%= 'class="complete"' if note.complete %>>
    <p>
      <%= note.content %>
      <span><a href="/<%= note.id %>">[edit]</a></span>
    </p>
    <p class="links">
      <a href="/<%= note.id %>/complete">&#8623;</a>
    </p>
    <p class="meta">Created: <%= note.created_at %></p>
  </article>
<% end %>

On the first line we begin a loop through each of the @notes (alternatively, we could have wrote for note in @notes, but using a block, as we are here, is a better practice). On line 2, we give the <article> a class of complete if the current note is set to complete. The rest should be pretty straight forward.


Editing a Note

So we can add and view notes. Now we just need the ability to edit and delete them.

You may have noticed that in our home.erb view we set an [edit] link for each note to what is essentially /:id, so let’s create that route now:

get '/:id' do
  @note = Note.get params[:id]
  @title = "Edit note ##{params[:id]}"
  erb :edit
end

We retrieve the requested note from the database using the ID provided, set up a @title variable, and load the views/edit.erb view file through the ERB parser.

Enter the following for the views/edit.erb view:

<% if @note %>
  <form action="/<%= @note.id %>" method="post" id="edit">
    <input type="hidden" name="_method" value="put">
    <textarea name="content"><%= @note.content %></textarea>
    <input type="checkbox" name="complete" <%= "checked" if @note.complete %>>
    <input type="submit">
  </form>
  <p><a href="/<%= @note.id %>/delete">Delete</a></p>
<% else %>
  <p>Note not found.</p>
<% end %>

This is a fairly simple view. A form which points back to the current page, a textarea containing the content of the note and a checkbox which gets checked if the note is set to complete.

But look at the third line. Mysterious. To explain this, we need to side-track a little.

RESTful Services

You’ve heard of the two terms GET and POST.

  • GET: The most common. It’s generally for requesting a page, and can be bookmarked.
  • POST: Used for submitting data and can not be bookmarked.

But GET and POST aren’t the only “HTTP verbs” – there’s two more you should know about: PUT and DELETE.

Technically, POST should only be used for creating something – like creating a new Note in your awesome new web app, for example.

PUT is the verb for modifying something. And DELETE, you guessed it, is for deleting something.

Having these four verbs is a great way to separate an app up. It’s logical. Unfortunately, web browsers don’t actually support PUT or DELETE requests, which is why you’ve likely never heard of them before.

So, getting back on track here, if we want to logically split our app up (which Sinatra encourages), we have to fake these PUT and DELETE requests. You’ll see our form’s action is set to post. The hidden _method input field which we’ve set to put on the third line lets Sinatra fake this PUT request, while actually using a POST. Rails, among other frameworks, do things a similar way.


Let us PUT

Now we’ve faked our PUT request, we can create a route for it:

put '/:id' do
  n = Note.get params[:id]
  n.content = params[:content]
  n.complete = params[:complete] ? 1 : 0
  n.updated_at = Time.now
  n.save
  redirect '/'
end

It’s all pretty simple. We get the relevant note using the ID in the URI, set the fields to the new values, save, and redirect home. Notice how on the fourth line we’re using a ternary operator to set n.complete to 1 if params[:complete] exists, or 0 otherwise. This is because the value of a checkbox is only submitted with a form if it is checked, so we’re simply checking for the existence of it.


Deleting a Note

In our edit.erb view, we added a ‘Delete’ link to what is essentially the path /:id/delete. Add this to your application file:

get '/:id/delete' do
  @note = Note.get params[:id]
  @title = "Confirm deletion of note ##{params[:id]}"
  erb :delete
end

On this page we’ll get confirmation from the user that they actually want to delete this note. Create the view file at views/delete.erb with the following:

<% if @note %>
  <p>Are you sure you want to delete the following note: <em>"<%= @note.content %>"</em>?</p>
  <form action="/<%= @note.id %>" method="post">
    <input type="hidden" name="_method" value="delete">
    <input type="submit" value="Yes, Delete It!">
    <a href="/<%= @note.id %>">Cancel</a>
  </form>
<% else %>
  <p>Note not found.</p>
<% end %>

Note that just like how we faked a PUT request by setting a hidden _method input field, we’re now faking a DELETE request.


The DELETE Route

I’m sure you’re getting the hang of this by now. The delete route is:

delete '/:id' do
  n = Note.get params[:id]
  n.destroy
  redirect '/'
end

Try it out! You should now be able to view, add, edit and remove notes. There’s just one more thing…


Marking a Note as “Complete”

Right now if you want to set a note as complete you have to go into the Edit view and check the box on that page. Let’s make that process a bit simpler.

Back when we set up the main home page, we included a /:id/complete link on each note. Let’s make that route now, which will simply set a note as complete (or incomplete if it was already set to complete):

get '/:id/complete' do
  n = Note.get params[:id]
  n.complete = n.complete ? 0 : 1 # flip it
  n.updated_at = Time.now
  n.save
  redirect '/'
end

Conclusion

You and Sinatra pull off one crackin’ duet! You’ve very quickly written a simple web app which performs all the CRUD operations you’d expect an app to do. It’s written in super-sexy-clean Ruby code, and is separated into its logical parts.

In the final part of Singing with Sinatra, the Encore, we’ll improve on error handling, secure the app from XSS and create an RSS feed for the notes.

Note: You can browse the final project files for this tutorial over at GitHub.

Dan Harper is danharper on Themeforest
Note: Want to add some source code? Type <pre><code> before it and </code></pre> after it. Find out more
  • http://youfounderic.com Eric Kelly

    Keep the Sinatra coming!

  • http://aaroncruz.com aaron

    Great tut. Seeing stuff like this really helps me organize my wild west-like sinatra code.
    There is a weird emoticon error at the beginning of the “The Home Page” section where it adds an image tag after you call to Notes.all.

    • http://www.danharper.me Dan Harper
      Author

      Damn WordPress :) I’ll get that fixed, thanks!

  • filip

    Nice work Dan. I like the Sinatra nettuts series. Sinatra is a great framework. For larger projects i have created a project template https://github.com/sirfilip/sinatra-mvc-template for larger sinatra apps. It uses bundler and autoloading for dependency management. Check it out or fork it and please tell me if you like it.

    • http://www.danharper.me Dan Harper
      Author

      That sounds interesting, I’ll check it out :)

  • JulianN

    You need to edit the first example, it has had the :o of :order turned into <img src=…

    All the Best
    Julian

    • http://www.danharper.me Dan Harper
      Author

      Fixed.. sort of. WordPress is completely butchering it!

  • Dan Kubb

    Great tutorial! I’ve just got a few DataMapper specific suggestions:

    You might want to change the gem install line to “gem install datamapper dm-sqlite-adapter”, the sqlite3 driver isn’t used by DM, and dm-sqlite-adapter will bring in the correct database driver for you. Also, since you’re requiring the “datamapper” gem in the example, you probably want to install that instead of just the “dm-core” gem.

    For the boolean property you probably want to specify “:default => false”. In general with DM you’re working at the level of the objects, and DM takes care of the implementation specific issues, like using 0 or 1 to represent true/false in some DBs.

    You generally want to finalize before calling “DataMapper.auto_upgrade!” or “DataMapper.auto_migrate!”. The general convention is to chain them like “DataMapper.finalize.auto_upgrade!”.

    The third code example has expanded the order option into a smilie ;)

    You can use the dm-timestamps plugin to automatically update the created_at and updated_at attributes each time the record is saved saving you from having to explicitly set them.

    DataMapper should take care of coercing the “compete” parameter into true/false without you having to use the “params[:complete] ? 1 : 0″ condition. Just use “n.complete = params[:complete]“, or better yet combine your setting and saving into “n.update(params)”, assuming you’ve rejected any unknown params.

    • http://www.danharper.me Dan Harper
      Author

      Thanks for the feedback! I thought I had changed the gem install line to install “datamapper” instead of just “dm-core”, I’ll get that changed!

      I did not know about “DataMapper.finalize.auto_upgrade!”, though. And I’ll get that code example fixed, damn WordPress!

      As for the “complete” param, I believe I tried that first, but something threw an error because browsers only submit a checkbox in a form if it was checked. So we were trying to add an inexistant parameter.

      I try to keep my examples as accurate as possible, so thanks again for the feedback :)

  • Zeppelin

    1. You’re using hard tabs instead of soft tabs (spaces). The “ruby-way” is 2 spaces. (http://pub.cozmixng.org/~the-rwiki/rw-cgi.rb?cmd=view;name=RubyCodingConvention – click “Preview”)
    2. The Gemfile is incomplete: dm-sqlite-adapter is missing.
    3. Also it might be a good idea to note: some users may need to run the application through Bundler (bundle exec rackup config.ru)

    Otherwise nice tutorial ;)

    • http://www.danharper.me Dan Harper
      Author

      I usually do use two spaces. My text editor may have interfered with that though (it’s set to display tabs as two spaces, so I don’t usually see the difference, I’ll look into reconfiguring it).

      Ah, I didn’t intend on including the Gemfile with the source code, but oh well.

  • Prakhar

    Thank you so much for this Dan!

  • Nelson

    Had initial issues with some dependencies. Had to use the ‘dm-core’ instead of ‘datamapper’ and also require ‘dm-migrations’ for ‘auto_upgrade!’ method to work.

    But overall, great tutorial and keep them coming please!

    Thank you

    • http://www.danharper.me Dan Harper
      Author

      Yeah, sorry, I had meant to write ‘gem install datamapper’, not ‘gem install dm-core’. I’ve fixed the post now so it should be fine.

  • Prakhar

    Dan

    Can you please do a quick tip session on how to host your apps made in RoR or Sinatra online? And how to configure sqlite or any other DB on a server. While there are great tuts on nettuts on how to make web apps and test them locally I couldn’t find anything that will guide me how to host it online on a web server.
    It’ll be wonderful if you can cover that sometime.

    Thanks

    • http://youfounderic.com Eric Kelly

      One way to get you Sinatra app up quickly onto the web is to use Heroku. This one guy has written a good tutorial about how to get it done on his blog (along with a bunch of other great stuff). Check out the post specific to deploying on Heroku at http://ididitmyway.heroku.com/past/2010/1/16/deploying_sinatra_apps_on_heroku/

    • http://www.danharper.me Dan Harper
      Author

      Sure, I’ll get a tutorial or quick tip done soon. The easiest way is to use Passenger (http://www.modrails.com/), an extension for the Apache and Nginx webservers to allow you to host Ruby web apps built on Rack (so including RoR and Sinatra).

      Or as Eric stated, Heroku is a really great Git-powered hosting platform which, if you’ve used Git before, makes publishing your site as easy as ‘git push heroku’. They have free packages available so definitely check them out!

  • http://www.skolenirails.info Droid

    Really nice, Dan :-) Is there any simple way to make adding ajax?

  • Nico

    Great stuff, please make more of these :) Even premium tutorial would be nice, take some larger scale project from start (design) to finish (usable web-application).

    Learned a lot just by following this along, and this really sparked an interest in Sinatra for me.

  • http://www.potamocheri.eu/ TED©

    The Put method return ALWAYS a “Not Found” page. Why?

  • paul

    I don’t quite understand when the property “:content” is set to true in the code.

  • sohussain

    thanks a lot for the tutorial!!

    i just had to make one change in “recall.rb”
    instead of
    require ‘datamapper’

    i had to use
    require ‘data_mapper’

  • Nico

    Hi
    require ‘datamapper’ should read:
    require ‘data_mapper’

    great tut!

  • http://tech.ctkschool.org Michael

    Yeah!

    require ‘data_mapper’

    That underscore cost me an hour!

    • http://www.terran.birrell.us Terran

      Yeah, require ‘data_mapper’ instead of require ‘datamapper’ was a bit of a head scratcher for me too. Would be handy if that were updated in the tutorial.

    • baiki

      Yeah… me too. An explanation would be cool :-)

      Well done, thanks.

      Cheers!

  • greg
  • Tatiana

    Some of this seems to not work if you don’t use the root path.

    For example, I have a path like this: “localhost:4567/test_notes”

    The above works to submit notes but not to edit them. When I edit them, I try to use this url:

    localhost:4567/test_notes/4

    It never works. So as an example, I’ll have this:

    put ‘/test_notes/:id’ do
    note = Note.get params[:id]
    note.content = params[:content]
    note.complete = params[:complete] ? 1 : 0
    note.updated_at = Time.now
    note.save
    redirect ‘/test_notes’
    end

    On my edit page I’ll have this:

    form action=”/test_notes/” method=”post” id=”edit”
    input type=”hidden” name=”_method” value=”put”
    textarea name=”content”></textarea
    input type="submit"
    /form

    I'm not sure why it doesn't work, but I just get a 404 when trying to submit an edit. It goes to the correct url:

    http://localhost:4567/test_notes/4

    if I'm editing the fourth note I put in. But the above gets a 404. Strangely, if I then refresh it works. I don't know if this is something I'm doing wrong or something that doesn't work with Sinatra.

    In a nutshell, all I did is replace everything that went to root "/" with "/test_notes" and set my paths up accordingly.

  • Etay Luz

    datamapper should be data_mapper

    • jmac

      YES!

      I got bogged down for most of a day trying to solve this one because I didn’t understand why require “datamapper” didn’t work. Of course, while trying to debug this, I learned a lot about rvm and gem and my environment configuration setup (and some minor problems I had with it) but it wasn’t the problem at all.

      I’m also still trying to understand what the difference between “datamapper” and “data_mapper” is, but, with rvm 1.16.17 and ruby 1.9.3-p286, you must gem install data_mapper (or change the sqlite3 gem install to gem install sqlite3 data_mapper dm-sqlite-adapter ) and use require “data_mapper” else you’ll get bocked with this tutorial like I did.

      • http://twitter.com/UBC_founder Andy Brown

        @jmac think I just had the exact same ‘most of a day’ experience as you…. loving rvm though far better than previous set up.

    • http://www.facebook.com/profile.php?id=199701147 Stanislav Beremski

      Yep. I found this also.

    • walid

      yep !

  • http://zegmaarwim.blogspot.com Wim

    For those who are not familiar with the ternary operator:

    n.complete = params[:complete] ? 1 : 0

    means that, if params[:complete] is true, than the value of n.complete should become the first value after the question mark (in this example) 1. Else it should become the value after the colon, being 0 in this example.

    So, when we go to the part of ‘get ‘/:id/complete’ do‘, this means that

    n.complete = n.complete ? 0 : 1 # flip it

    will result in flipping the initial value of n.complete, as the ternary operator tests whether n.complete is true (or “1″ if it is a numeric value), if so, then the result will be 0. If the initial value was not true, than it will now become 1.

    Hopefully this clarifies the ternary operator?

  • http://softwareodyssey.com gr

    Great series Dan!
    Maybe it’s explained somewhere else, or I’m missing a concept, but how does the layout.erb magically get applied to the pages? I’m guessing it’s by convention, and maybe there’s something in the docs and I should just rtfm.

    And do assets like stylesheets, images and .js files need to reside in /public (by convention also?) or can they be specifically referenced like href=’js/…’ instead of href=’/…’?

    Thanks, and keep sinatra stuff coming.

  • Orestis

    Hi Dan and thanks for the great tutorial,

    I am just facing a new problem. I installed the sqlite3 gem and every time I tried to run recall.rb a window pops up which says: ruby.exe – System Error “The program can’t start because sqlite3.dll is missing from your computer. Try reinstalling the program to fix this problem.”.

    I have downloaded the sqlite3.dll from http://www.sqlite.org/sqlitedll-3_7_2.zip and copy it into C:\Windows\System32 but nothing changed. I am stuck at this point for over 2 hours and google has few to say about it… Please help :)!

    Orestis

    • Orestis

      Thank god I found solution after 20 minutes. I just copied the sqlite3.dll at C:\Windows\system and it totally worked :)!!

      • http://www.facebook.com/profile.php?id=199701147 Stanislav Beremski

        Thanks!

  • http://www.facebook.com/profile.php?id=199701147 Stanislav Beremski

    Thanks for this Dan. Great tutorial!

  • walid

    that is COO..while(1)..OOL

  • Scott Chilton

    I found it necessary to add[ use Rack::MethodOverride ] to my config.ru file for Sinatra to recognize the PUT and DELETE requests and not return the “Sinatra does not know this ditty.”

  • http://twitter.com/JHellings Justin Hellings

    In the “Adding a Note to the Database” section, n.complete must be set to some value. Otherwise the save will fail and you won’t find out until you search the net and find out that, after a failing DataMapper save, you can iterate over a collection of error objects in the newly created Note object.

  • rp

    change require ‘datamapper’ to require ‘data_mapper’

  • bruce

    I kept getting this error running Ruby 1.9.1 on Ubuntu:

    ERROR: Error installing datamapper:
    ERROR: Failed to build gem native extension.

    Turned out I needed to install the development package:
    sudo apt-get install ruby1.9.1-dev

    That fixed the error.

  • abizern

    If you’re having trouble with the edit view always having a nil note, it’s because the parameter is a string and you need to convert it to an int. So in the route for “get ‘/:id’” the line that gets the note should be:

    @note = Note.get params[:id].to_i