We are building validations into our Reddit app, closing a voting loophole, and refactoring.

1. Clone Reddit-In-Sinatra - 1.0

1
git clone git://github.com/DruRly/reddit-in-sinatra.git -b 1.0.x

2. Install Gems

1
2
cd reddit-in-sinatra
bundle install

3. Open reddit.rb

reddit.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
require 'sinatra'
require 'data_mapper'
require 'haml'
require 'sinatra/reloader'

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

class Link
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :url, Text
  property :score, Integer
  property :points, Integer, :default => 0
  property :created_at, Time

  attr_accessor :score

  def calculate_score
    time_elapsed = (Time.now - self.created_at) / 3600
    self.score = ((self.points-1) / (time_elapsed+2)**1.8).real
  end

  def self.all_sorted_desc
    self.all.each { |item| item.calculate_score }.sort { |a,b| a.score <=> b.score }.reverse
  end
end

DataMapper.finalize.auto_upgrade!

get '/' do
  @links = Link.all :order => :id.desc
  haml :index
end

get '/hot' do
  @links = Link.all_sorted_desc
  haml :index
end

post '/' do
  l = Link.new
  l.title = params[:title]
  l.url = params[:url]
  l.created_at = Time.now
  l.save
  redirect back
end

put '/:id/vote/:type' do
  l = Link.get params[:id]
  l.points += params[:type].to_i
  l.save
  redirect back
end

4. Start Server

1
ruby reddit.rb

Starting the server allows us to interact with the application as we complete each step.

The application will be available at localhost:4567.

5. Require Gems on One Line

1
2
3
4
require 'sinatra'
require 'data_mapper'
require 'haml'
require 'sinatra/reloader'

becomes..

1
%w{sinatra data_mapper haml sinatra/reloader}.each { |lib| require lib}

We are iterating over each gem which eliminates duplicate require statements.

6. Remove score Property

1
2
3
4
5
6
7
8
...
  property :id, Serial
  property :title, String
  property :url, Text
  property :score, Integer
  property :points, Integer, :default => 0
  property :created_at, Time
...

becomes..

1
2
3
4
5
6
7
...
  property :id, Serial
  property :title, String
  property :url, Text
  property :points, Integer, :default => 0
  property :created_at, Time
...

We are removing the score property because it is unnecesary.

7. Add Presence Validation

1
2
3
4
...
  property :title, String
  property :url, Text
...

becomes..

1
2
3
4
...
  property :title, String, :required => true
  property :url, Text, :required => true
...

We are requring the presense of title and url properties before records are created.

8. Add Format Validation

1
  property :url, Text, :required => true

becomes..

1
  property :url, Text, :required => true, :format => :url

We are adding regex matching to prevent the creation of links with invalid url formats.

9. Refactor Post

1
2
3
4
5
6
7
8
post '/' do
  l = Link.new
  l.title = params[:title]
  l.url = params[:url]
  l.created_at = Time.now
  l.save
  redirect back
end

becomes..

1
2
3
4
post '/' do
  Link.create(:title => params[:title], :url => params[:url], :created_at => Time.now)
  redirect back
end

We are simplifying post by using create instead of new and save.

10. Refactor Put

1
2
3
4
5
6
put '/:id/vote/:type' do
  l = Link.get params[:id]
  l.points += params[:type].to_i
  l.save
  redirect back
end

becomes..

1
2
3
4
5
put '/:id/vote/:type' do
  l = Link.get params[:id]
  l.update(:points => l.points + params[:type].to_i)
  redirect back
end

We are consolidating two functions with update.

11. Close Voting Loophole

1
2
3
4
5
put '/:id/vote/:type' do
  l = Link.get params[:id]
  l.update(:points => l.points + params[:type].to_i)
  redirect back
end

becomes..

1
2
3
4
5
6
7
put '/:id/vote/:type' do
  if params[:type].to_i.abs == 1
    l = Link.get params[:id]
    l.update(:points => l.points + params[:type].to_i)
  end
  redirect back
end

We are using the if modifier to ensure that :type is +/-1.

Final Result

reddit.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
%w{sinatra data_mapper haml sinatra/reloader}.each { |lib| require lib}

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

class Link
  include DataMapper::Resource
  property :id, Serial
  property :title, String, :required => true
  property :url, Text, :required => true, :format => :url
  property :points, Integer, :default => 0
  property :created_at, Time

  attr_accessor :score

  def calculate_score
      time_elapsed = (Time.now - self.created_at) / 3600
      self.score = ((self.points-1) / (time_elapsed+2)**1.8).real
  end

  def self.all_sorted_desc
    self.all.each { |item| item.calculate_score }.sort { |a,b| a.score <=> b.score }.reverse
  end
end
DataMapper.finalize.auto_upgrade!

get '/' do
  @links = Link.all :order => :id.desc
  haml :index
end

get '/hot' do
  @links = Link.all_sorted_desc
  haml :index  
end

post '/' do
  Link.create(:title => params[:title], :url => params[:url], :created_at => Time.now)
  redirect back
end

put '/:id/vote/:type' do
  if params[:type].to_i.abs == 1
    l = Link.get params[:id]
    l.update(:points => l.points + params[:type].to_i)
  end
  redirect back
end

Github

Version 1.0 Github | Tutorial

Version 2.0 Github | Tutorial

Credits

Comments