diff --git a/app/assets/javascripts/sessions.js b/app/assets/javascripts/sessions.js
new file mode 100644
index 0000000..dee720f
--- /dev/null
+++ b/app/assets/javascripts/sessions.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/javascripts/users.js b/app/assets/javascripts/users.js
new file mode 100644
index 0000000..dee720f
--- /dev/null
+++ b/app/assets/javascripts/users.js
@@ -0,0 +1,2 @@
+// Place all the behaviors and hooks related to the matching controller here.
+// All this logic will automatically be available in application.js.
diff --git a/app/assets/stylesheets/sessions.css b/app/assets/stylesheets/sessions.css
new file mode 100644
index 0000000..afad32d
--- /dev/null
+++ b/app/assets/stylesheets/sessions.css
@@ -0,0 +1,4 @@
+/*
+ Place all the styles related to the matching controller here.
+ They will automatically be included in application.css.
+*/
diff --git a/app/assets/stylesheets/users.css b/app/assets/stylesheets/users.css
new file mode 100644
index 0000000..afad32d
--- /dev/null
+++ b/app/assets/stylesheets/users.css
@@ -0,0 +1,4 @@
+/*
+ Place all the styles related to the matching controller here.
+ They will automatically be included in application.css.
+*/
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d83690e..b3c6a3d 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -2,4 +2,34 @@ class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
+
+ private
+
+ def current_user
+ @current_user ||= User.find(session[:user_id]) if session[:user_id]
+ end
+
+ def user_signed_in?
+ !current_user.nil?
+ end
+
+ def user_signed_out?
+ current_user.nil?
+ end
+
+ def sign_in!(user)
+ session[:user_id] = user.id
+ @current_user = user
+ end
+
+ def sign_out!
+ @current_user = nil
+ session.delete(:user_id)
+ end
+
+ def authorize
+ redirect_to login_url, alert: 'Please log in to access that.' if user_signed_out?
+ end
+
+ helper_method :current_user, :user_signed_in?, :user_signed_out?, :sign_in!, :sign_out!, :authorize
end
diff --git a/app/controllers/links_controller.rb b/app/controllers/links_controller.rb
index cc2ec1d..81e74d5 100644
--- a/app/controllers/links_controller.rb
+++ b/app/controllers/links_controller.rb
@@ -1,7 +1,13 @@
class LinksController < ApplicationController
+ before_filter :authorize, only: [:destroy]
+
# GET /links
def index
- @links = Link.order('created_at DESC')
+ if current_user
+ @links = current_user.links
+ else
+ @links = Link.where(user_id: nil).order('created_at DESC')
+ end
end
# GET /l/:short_name
@@ -10,9 +16,10 @@ def show
@link = Link.find_by_short_name(params[:short_name])
if @link
+ @link.clicked!
redirect_to @link.url
else
- render text: "No such link.", status: 404
+ render text: 'No such link.', status: 404
end
end
@@ -24,6 +31,7 @@ def new
# POST /links
def create
@link = Link.new(link_params)
+ @link.user_id = current_user.id if user_signed_in?
if @link.save
redirect_to root_url, notice: 'Link was successfully created.'
@@ -32,9 +40,16 @@ def create
end
end
- private
- # Only allow a trusted parameter "white list" through.
- def link_params
- params.require(:link).permit(:url)
+ def destroy
+ @link = Link.find_by_short_name(params[:short_name])
+ @link.destroy
+
+ redirect_to action: :index, notice: 'Link deleted!'
end
+
+ private
+ # Only allow a trusted parameter "white list" through.
+ def link_params
+ params.require(:link).permit(:url, :shortname)
+ end
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
new file mode 100644
index 0000000..27ddd2e
--- /dev/null
+++ b/app/controllers/sessions_controller.rb
@@ -0,0 +1,38 @@
+class SessionsController < ApplicationController
+ def show
+ end
+
+ def new
+ @user = User.new
+ end
+
+ def edit
+ end
+
+ def create
+ @user = User.find_by_email(params[:email])
+
+ if @user && @user.authenticate(params[:password])
+ sign_in!(@user)
+ redirect_to root_url, notice: 'Logged in!'
+ else
+ ### IS THERE A BETTER WAY TO SHOW A LOGIN ERROR?
+ @user = User.new
+ @user.errors.add(:base, 'That email and password were not valid! Please try again.')
+ render :new
+ end
+ end
+
+ def update
+ end
+
+ def destroy
+ session[:user_id] = nil
+ redirect_to root_url, notice: 'Logged out!'
+ end
+
+ private
+ def session_params
+ # params.require(:session).permit(???)
+ end
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
new file mode 100644
index 0000000..f435a7d
--- /dev/null
+++ b/app/controllers/users_controller.rb
@@ -0,0 +1,36 @@
+class UsersController < ApplicationController
+ def index
+ end
+
+ def show
+ end
+
+ def new
+ @user = User.new
+ end
+
+ def edit
+ end
+
+ def create
+ @user = User.new(user_params)
+
+ if @user.save
+ sign_in!(@user)
+ redirect_to root_url, notice: 'Welcome to url-shortener!'
+ else
+ render :new
+ end
+ end
+
+ def update
+ end
+
+ def destroy
+ end
+
+ private
+ def user_params
+ params.require(:user).permit(:email, :password, :password_confirmation)
+ end
+end
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
new file mode 100644
index 0000000..309f8b2
--- /dev/null
+++ b/app/helpers/sessions_helper.rb
@@ -0,0 +1,2 @@
+module SessionsHelper
+end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
new file mode 100644
index 0000000..2310a24
--- /dev/null
+++ b/app/helpers/users_helper.rb
@@ -0,0 +1,2 @@
+module UsersHelper
+end
diff --git a/app/models/link.rb b/app/models/link.rb
index 7972816..f6919ca 100644
--- a/app/models/link.rb
+++ b/app/models/link.rb
@@ -1,8 +1,22 @@
class Link < ActiveRecord::Base
- before_create :set_short_name
+ before_validation :set_short_name, :validate_url_and_prepend_scheme_if_none!
validates :url, :presence => true
+ validates :clicks_count,
+ :numericality => {
+ :only_integer => true,
+ :greater_than_or_equal_to => 0
+ },
+ :presence => true
+
+ belongs_to :user
+
+ def clicked!
+ self.clicks_count += 1
+ self.save
+ end
+
# This controls how an ActiveRecord object is displayed in a URL context.
# This way, if we do link_path(@link), Rails will generate a path like
# "/l/#{@link.short_name}" vs. "/l/#{@link.id}".
@@ -12,19 +26,26 @@ def to_param
end
private
- def set_short_name
- # Generate and assign a random short_name unless one has already been set.
- return self.short_name if self.short_name.present?
-
- # See: http://www.ruby-doc.org/stdlib-2.1.2/libdoc/securerandom/rdoc/SecureRandom.html#method-c-urlsafe_base64
- # We do this to ensure we're not creating two links with the same short_name
- # Since it's randomly generated and not user-supplied, we can't rely on
- # validations to do this for us.
- try_short_name = SecureRandom.urlsafe_base64(6)
- while Link.where(:short_name => try_short_name).any?
- try_short_name = SecureRandom.urlsafe_base64(6)
+ def validate_url_and_prepend_scheme_if_none!
+ uri = URI.parse(url)
+ url.prepend("http://") unless uri.kind_of?(URI::HTTP) || uri.kind_of?(URI::HTTPS)
+ rescue URI::BadURIError, URI::InvalidURIError
+ self.errors.add(:url, 'is not a valid URL')
end
- self.short_name = try_short_name
- end
+ def set_short_name
+ # Generate and assign a random short_name unless one has already been set.
+ return self.short_name if self.short_name.present?
+
+ # See: http://www.ruby-doc.org/stdlib-2.1.2/libdoc/securerandom/rdoc/SecureRandom.html#method-c-urlsafe_base64
+ # We do this to ensure we're not creating two links with the same short_name
+ # Since it's randomly generated and not user-supplied, we can't rely on
+ # validations to do this for us.
+ try_short_name = SecureRandom.urlsafe_base64(6)
+ while Link.where(:short_name => try_short_name).any?
+ try_short_name = SecureRandom.urlsafe_base64(6)
+ end
+
+ self.short_name = try_short_name
+ end
end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..d53e4d9
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,6 @@
+class User < ActiveRecord::Base
+ has_secure_password
+ validates :email, :presence => true, :uniqueness => true
+
+ has_many :links
+end
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index dd67306..69ec005 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -4,6 +4,7 @@
UrlShortener
<%= stylesheet_link_tag 'application', media: 'all' %>
<%= csrf_meta_tags %>
+ <%= javascript_include_tag "application" %>
diff --git a/app/views/links/_form.html.erb b/app/views/links/_form.html.erb
index 4dfa201..e3f076a 100644
--- a/app/views/links/_form.html.erb
+++ b/app/views/links/_form.html.erb
@@ -1,7 +1,7 @@
-<%= form_for(@link) do |f| %>
+<%= form_for @link do |f| %>
<% if @link.errors.any? %>
-
<%= pluralize(@link.errors.count, "error") %> prohibited this link from being saved:
+
<%= pluralize(@link.errors.count, "error") %> found:
<% @link.errors.full_messages.each do |message| %>
diff --git a/app/views/links/index.html.erb b/app/views/links/index.html.erb
index 92c5393..2ac446f 100644
--- a/app/views/links/index.html.erb
+++ b/app/views/links/index.html.erb
@@ -6,13 +6,22 @@
<% end %>
+<% if user_signed_in? %>
+ Logged in as <%= current_user.email %>.
+ <%= link_to 'Log Out', logout_path, :method => :delete %>
+ <% else %>
+ <%= link_to 'Log In', login_path %> or
+ <%= link_to 'Sign Up', new_user_path %>
+ <% end %>
+
+
| Added |
Short URL |
URL |
- |
+ Clicks |
@@ -22,7 +31,11 @@
<%= time_ago_in_words(link.created_at) %> ago |
<%= link_url(link) %> |
<%= link.url %> |
- <%= link_to 'Visit link', link %> |
+ <%= link.clicks_count %> |
+ <%= link_to 'Visit Link', link, :target => '_blank' %> |
+ <% if user_signed_out? || (user_signed_in? && link.user == current_user) %>
+ <%= link_to 'Delete Link', link, :method => :delete %> |
+ <% end %>
<% end %>
diff --git a/app/views/links/new.html.erb b/app/views/links/new.html.erb
index 64c66bf..d188ace 100644
--- a/app/views/links/new.html.erb
+++ b/app/views/links/new.html.erb
@@ -1,4 +1,4 @@
-New link
+New Link
<%= render 'form' %>
diff --git a/app/views/sessions/_form.html.erb b/app/views/sessions/_form.html.erb
new file mode 100644
index 0000000..2b909ef
--- /dev/null
+++ b/app/views/sessions/_form.html.erb
@@ -0,0 +1,26 @@
+
+<%= form_tag login_path do %>
+ <% if @user.errors.any? %>
+
+
<%= pluralize(@user.errors.count, "error") %> found:
+
+
+ <% @user.errors.full_messages.each do |message| %>
+ - <%= message %>
+ <% end %>
+
+
+ <% end %>
+
+
+ <%= label_tag :email %>
+ <%= text_field_tag :email, params[:email] %>
+
+
+ <%= label_tag :password %>
+ <%= password_field_tag :password %>
+
+
+ <%= submit_tag "Log In" %>
+
+<% end %>
diff --git a/app/views/sessions/new.html.erb b/app/views/sessions/new.html.erb
new file mode 100644
index 0000000..7e50ed8
--- /dev/null
+++ b/app/views/sessions/new.html.erb
@@ -0,0 +1,3 @@
+Log In
+
+<%= render 'form' %>
diff --git a/app/views/users/_form.html.erb b/app/views/users/_form.html.erb
new file mode 100644
index 0000000..cf7875f
--- /dev/null
+++ b/app/views/users/_form.html.erb
@@ -0,0 +1,29 @@
+<%= form_for @user do |f| %>
+ <% if @user.errors.any? %>
+
+
<%= pluralize(@user.errors.count, "error") %> found:
+
+
+ <% @user.errors.full_messages.each do |message| %>
+ - <%= message %>
+ <% end %>
+
+
+ <% end %>
+
+
+ <%= f.label :email %>
+ <%= f.text_field :email %>
+
+
+ <%= f.label :password %>
+ <%= f.password_field :password %>
+
+
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation %>
+
+
+ <%= f.submit %>
+
+<% end %>
diff --git a/app/views/users/new.html.erb b/app/views/users/new.html.erb
new file mode 100644
index 0000000..0049504
--- /dev/null
+++ b/app/views/users/new.html.erb
@@ -0,0 +1,6 @@
+
+Sign Up
+
+<%= render 'form' %>
+
+<%= link_to 'Back', links_path %>
diff --git a/config/routes.rb b/config/routes.rb
index 7b8c84c..de619d6 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -12,10 +12,20 @@
# !!!IMPORTANT!!!
Rails.application.routes.draw do
+ root to: 'links#index'
+ get '/links/new', to: 'links#new', as: 'new_link'
+ post '/links', to: 'links#create', as: 'links'
+ get '/l/:short_name', to: 'links#show', as: 'link'
+ delete '/l/:short_name', to: 'links#destroy'
+
+ get '/login', to: 'sessions#new'
+ post '/login', to: 'sessions#create'
+ delete '/logout', to: 'sessions#destroy'
+
+ resources :users
+
# Often you'll see this:
#
- # resources :links
- #
# This is short-hand for the following routes:
#
# get '/links', to: 'links#index', as: 'links'
@@ -29,12 +39,7 @@
#
# For our first application, we can only create and view links,
# so these will be our routes.
-
- root to: 'links#index'
- get '/links/new', to: 'links#new', as: 'new_link'
- post '/links', to: 'links#create', as: 'links'
- get '/l/:short_name', to: 'links#show', as: 'link'
-
+ #
# "get" tells Rails the HTTP method to look for (GET, in this case)
# "/l/:short_name" tells Rails the URL pattern(s) to look for
# "to: 'links#show'" tells Rails to call the show method on links_controller
diff --git a/db/migrate/20150403233303_add_click_count_to_links.rb b/db/migrate/20150403233303_add_click_count_to_links.rb
new file mode 100644
index 0000000..f1d9fbd
--- /dev/null
+++ b/db/migrate/20150403233303_add_click_count_to_links.rb
@@ -0,0 +1,5 @@
+class AddClickCountToLinks < ActiveRecord::Migration
+ def change
+ add_column :links, :clicks_count, :integer, :default => 0, :null => false
+ end
+end
diff --git a/db/migrate/20150412214354_create_users.rb b/db/migrate/20150412214354_create_users.rb
new file mode 100644
index 0000000..a20b75e
--- /dev/null
+++ b/db/migrate/20150412214354_create_users.rb
@@ -0,0 +1,11 @@
+class CreateUsers < ActiveRecord::Migration
+ def change
+ create_table :users do |t|
+ t.string :email
+ t.string :password_digest
+
+ t.timestamps null: false
+ end
+ add_index :users, :email, unique: true
+ end
+end
diff --git a/db/migrate/20150412214410_add_user_to_links.rb b/db/migrate/20150412214410_add_user_to_links.rb
new file mode 100644
index 0000000..fd18a7e
--- /dev/null
+++ b/db/migrate/20150412214410_add_user_to_links.rb
@@ -0,0 +1,5 @@
+class AddUserToLinks < ActiveRecord::Migration
+ def change
+ add_reference :links, :user, index: true, foreign_key: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 184cca6..02affb2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,15 +11,27 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20140725003600) do
+ActiveRecord::Schema.define(version: 20150412214410) do
- create_table "links", force: true do |t|
+ create_table "links", force: :cascade do |t|
t.string "short_name"
t.string "url"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "clicks_count", default: 0, null: false
+ t.integer "user_id"
end
add_index "links", ["short_name"], name: "index_links_on_short_name", unique: true
+ add_index "links", ["user_id"], name: "index_links_on_user_id"
+
+ create_table "users", force: :cascade do |t|
+ t.string "email"
+ t.string "password_digest"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ end
+
+ add_index "users", ["email"], name: "index_users_on_email", unique: true
end
diff --git a/spec/controllers/links_controller_spec.rb b/spec/controllers/links_controller_spec.rb
index ec03d45..8e9c765 100644
--- a/spec/controllers/links_controller_spec.rb
+++ b/spec/controllers/links_controller_spec.rb
@@ -24,7 +24,8 @@
# Link. As you add validations to Link, be sure to
# adjust the attributes here as well.
let(:valid_attributes) do
- {:url => 'http://example.com/widgets'}
+ {:url => 'http://example.com/widgets',
+ :clicks_count => 0}
end
let(:invalid_attributes) do
@@ -53,6 +54,14 @@
get :show, {:short_name => link.to_param}, valid_session
expect(response).to redirect_to(link.url)
end
+
+ it "increments clicks_count when link is clicked" do
+ link = Link.create! valid_attributes
+ expect {
+ get :show, {:short_name => link.to_param}, valid_session
+ link.reload
+ }.to change(link, :clicks_count).by(1)
+ end
end
describe "GET new" do
@@ -95,4 +104,6 @@
end
end
+ describe
+
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
new file mode 100644
index 0000000..7c73145
--- /dev/null
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe SessionsController, :type => :controller do
+
+end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
new file mode 100644
index 0000000..d3dada6
--- /dev/null
+++ b/spec/controllers/users_controller_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe UsersController, :type => :controller do
+
+end
diff --git a/spec/helpers/links_helper_spec.rb b/spec/helpers/links_helper_spec.rb
index cb69600..abc9515 100644
--- a/spec/helpers/links_helper_spec.rb
+++ b/spec/helpers/links_helper_spec.rb
@@ -11,5 +11,4 @@
# end
# end
RSpec.describe LinksHelper, :type => :helper do
- pending "add some examples to (or delete) #{__FILE__}"
end
diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb
new file mode 100644
index 0000000..48129e2
--- /dev/null
+++ b/spec/helpers/sessions_helper_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the SessionsHelper. For example:
+#
+# describe SessionsHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe SessionsHelper, :type => :helper do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb
new file mode 100644
index 0000000..0971a2f
--- /dev/null
+++ b/spec/helpers/users_helper_spec.rb
@@ -0,0 +1,15 @@
+require 'rails_helper'
+
+# Specs in this file have access to a helper object that includes
+# the UsersHelper. For example:
+#
+# describe UsersHelper do
+# describe "string concat" do
+# it "concats two strings with spaces" do
+# expect(helper.concat_strings("this","that")).to eq("this that")
+# end
+# end
+# end
+RSpec.describe UsersHelper, :type => :helper do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/link_spec.rb b/spec/models/link_spec.rb
index c9b9efe..9789c35 100644
--- a/spec/models/link_spec.rb
+++ b/spec/models/link_spec.rb
@@ -2,6 +2,8 @@
RSpec.describe Link, :type => :model do
describe '#valid?' do
+ it { should validate_numericality_of(:clicks_count).only_integer.is_greater_than_or_equal_to(0) }
+ it { should validate_presence_of(:clicks_count) }
it { should validate_presence_of(:url) }
end
@@ -15,6 +17,16 @@
end
end
+ describe '#clicked!' do
+ let(:link) { FactoryGirl.build(:link) }
+
+ it 'increments clicks_count by 1' do
+ expect {
+ link.clicked!
+ }.to change(link, :clicks_count).by(1)
+ end
+ end
+
describe '#to_param' do
let(:link) { FactoryGirl.create(:link) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
new file mode 100644
index 0000000..0bc0e60
--- /dev/null
+++ b/spec/models/user_spec.rb
@@ -0,0 +1,5 @@
+require 'rails_helper'
+
+RSpec.describe User, :type => :model do
+ pending "add some examples to (or delete) #{__FILE__}"
+end