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:

<% 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 %> +

+ - + @@ -22,7 +31,11 @@ - + + + <% if user_signed_out? || (user_signed_in? && link.user == current_user) %> + + <% 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:

+ + +
+ <% 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:

+ + +
+ <% 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
Added Short URL URLClicks
<%= 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' %><%= link_to 'Delete Link', link, :method => :delete %>