diff --git a/source/.gitignore b/source/.gitignore index 6a502e9..7b93474 100644 --- a/source/.gitignore +++ b/source/.gitignore @@ -14,3 +14,7 @@ # Ignore all logfiles and tempfiles. /log/*.log /tmp + +# Ignore vim tempfiles +*.swo +*.swp diff --git a/source/app/assets/stylesheets/scaffolds.css.scss b/source/app/assets/stylesheets/scaffolds.css.scss new file mode 100644 index 0000000..6ec6a8f --- /dev/null +++ b/source/app/assets/stylesheets/scaffolds.css.scss @@ -0,0 +1,69 @@ +body { + background-color: #fff; + color: #333; + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; +} + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; +} + +a { + color: #000; + &:visited { + color: #666; + } + &:hover { + color: #fff; + background-color: #000; + } +} + +div { + &.field, &.actions { + margin-bottom: 10px; + } +} + +#notice { + color: green; +} + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; +} + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px; + padding-bottom: 0; + margin-bottom: 20px; + background-color: #f0f0f0; + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + margin-bottom: 0px; + background-color: #c00; + color: #fff; + } + ul li { + font-size: 12px; + list-style: square; + } +} diff --git a/source/app/assets/stylesheets/urls.css.scss b/source/app/assets/stylesheets/urls.css.scss index a4281ec..7abea80 100644 --- a/source/app/assets/stylesheets/urls.css.scss +++ b/source/app/assets/stylesheets/urls.css.scss @@ -1,3 +1,14 @@ -// Place all the styles related to the Urls controller here. +// Place all the styles related to the urls controller here. // They will automatically be included in application.css. // You can use Sass (SCSS) here: http://sass-lang.com/ + +.urls { + table { + border-collapse: collapse; + tr { + td { + padding: 1em; + } + } + } +} diff --git a/source/app/controllers/urls_controller.rb b/source/app/controllers/urls_controller.rb index ef26710..783b53f 100644 --- a/source/app/controllers/urls_controller.rb +++ b/source/app/controllers/urls_controller.rb @@ -1,2 +1,81 @@ class UrlsController < ApplicationController + before_action :set_url, only: [:show, :edit, :update, :destroy] + + # GET /urls + # GET /urls.json + def index + @urls = Url.all + end + + # GET /urls/1 + # GET /urls/1.json + def show + end + + # GET /urls/new + def new + @url = Url.new + end + + # GET /urls/1/edit + def edit + end + + def goto + id = Url.decode_shortcode params[:shortcode] + url = Url.find id + url.click_count += 1 + url.save! + redirect_to url.destination + end + # POST /urls + # POST /urls.json + def create + @url = Url.new(url_params) + + respond_to do |format| + if @url.save + format.html { redirect_to @url, notice: 'Url was successfully created.' } + format.json { render :show, status: :created, location: @url } + else + format.html { render :new } + format.json { render json: @url.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /urls/1 + # PATCH/PUT /urls/1.json + def update + respond_to do |format| + if @url.update(url_params) + format.html { redirect_to @url, notice: 'Url was successfully updated.' } + format.json { render :show, status: :ok, location: @url } + else + format.html { render :edit } + format.json { render json: @url.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /urls/1 + # DELETE /urls/1.json + def destroy + @url.destroy + respond_to do |format| + format.html { redirect_to urls_url, notice: 'Url was successfully destroyed.' } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_url + @url = Url.find(params[:id]) + end + + # Never trust parameters from the scary internet, only allow the white list through. + def url_params + params.require(:url).permit(:shortcode, :destination) + end end diff --git a/source/app/helpers/urls_helper.rb b/source/app/helpers/urls_helper.rb index 83216b1..4c883ba 100644 --- a/source/app/helpers/urls_helper.rb +++ b/source/app/helpers/urls_helper.rb @@ -1,2 +1,3 @@ module UrlsHelper + end diff --git a/source/app/models/url.rb b/source/app/models/url.rb new file mode 100644 index 0000000..37a61a0 --- /dev/null +++ b/source/app/models/url.rb @@ -0,0 +1,22 @@ + +require 'socket' + +class Url < ActiveRecord::Base + validates :destination, url: true + + before_validation do + self.destination = "http://#{self.destination}" unless /^[a-z]+:/ =~ self.destination + end + + def shortcode + id.to_s(36) + end + + def self.decode_shortcode shortcode + shortcode.to_i(36) + end + + def shortlink + "http://localhost:3000/#{shortcode}" + end +end diff --git a/source/app/validators/url_validator.rb b/source/app/validators/url_validator.rb new file mode 100644 index 0000000..f37c484 --- /dev/null +++ b/source/app/validators/url_validator.rb @@ -0,0 +1,24 @@ + +require 'net/http' + +class UrlValidator < ActiveModel::EachValidator + + def validate_each(record, attribute, value) + uri = URI(value) + return record.errors.add attribute, "must use HTTP or HTTPS" unless ['http','https'].member? uri.scheme + begin + page_checker = Net::HTTP.new uri.host, uri.port + page_checker.use_ssl = uri.scheme.eql? "https" + page_checker.start do |http| + response = http.head (uri.path.empty? ? "/" : uri.path) + puts response.inspect + return record.errors.add attribute, "is not a valid address according to their server" unless response.code.to_i < 400 + end + rescue SocketError, Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError => e + return record.errors.add attribute, "could not be reached for validation" + end + rescue URI::InvalidURIError + record.errors[attribute] = "does not seem to be a valid URL" + end + +end diff --git a/source/app/views/layouts/application.html.erb b/source/app/views/layouts/application.html.erb index f946432..69c74e8 100644 --- a/source/app/views/layouts/application.html.erb +++ b/source/app/views/layouts/application.html.erb @@ -6,7 +6,7 @@ <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> <%= csrf_meta_tags %> -
+ <%= yield %> diff --git a/source/app/views/urls/_form.html.erb b/source/app/views/urls/_form.html.erb new file mode 100644 index 0000000..4dea0a9 --- /dev/null +++ b/source/app/views/urls/_form.html.erb @@ -0,0 +1,21 @@ +<%= form_for(@url) do |f| %> + <% if @url.errors.any? %> +| Shortcode | +Clicks | +Destination | ++ | ||
|---|---|---|---|---|---|
| <%= url.shortcode %> | +<%= url.click_count %> | +<%= link_to url.destination, url.destination %> | +<%= link_to 'Show', url %> | +<%= link_to 'Edit', edit_url_path(url) %> | +<%= link_to 'Destroy', url, method: :delete, data: { confirm: 'Are you sure?' } %> | +
<%= notice %>
+ ++ Destination: + <%= link_to @url.destination, @url.destination %> +
+ ++ Shortenened URL: + <%= link_to @url.shortlink, @url.shortlink %> +
+ ++ Clicks: + <%= @url.click_count %> +
+ +<%= link_to 'Edit', edit_url_path(@url) %> | +<%= link_to 'Back', urls_path %> diff --git a/source/app/views/urls/show.json.jbuilder b/source/app/views/urls/show.json.jbuilder new file mode 100644 index 0000000..bf7cf02 --- /dev/null +++ b/source/app/views/urls/show.json.jbuilder @@ -0,0 +1 @@ +json.extract! @url, :id, :shortcode, :destination, :created_at, :updated_at diff --git a/source/config/application.rb b/source/config/application.rb index 9a1de00..d337d0b 100644 --- a/source/config/application.rb +++ b/source/config/application.rb @@ -8,6 +8,7 @@ module Source class Application < Rails::Application + config.autoload_paths += %W["#{config.root}/app/validators/"] # Settings in config/environments/* take precedence over those specified here. # Application configuration should go into files in config/initializers # -- all .rb files in that directory are automatically loaded. diff --git a/source/config/routes.rb b/source/config/routes.rb index 3f66539..e0c9db1 100644 --- a/source/config/routes.rb +++ b/source/config/routes.rb @@ -1,4 +1,7 @@ Rails.application.routes.draw do + resources :urls + get ':shortcode' => 'urls#goto' + # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". diff --git a/source/db/migrate/20150126180838_create_urls.rb b/source/db/migrate/20150126180838_create_urls.rb new file mode 100644 index 0000000..d132adf --- /dev/null +++ b/source/db/migrate/20150126180838_create_urls.rb @@ -0,0 +1,10 @@ +class CreateUrls < ActiveRecord::Migration + def change + create_table :urls do |t| + t.string :shortcode + t.string :destination + + t.timestamps + end + end +end diff --git a/source/db/migrate/20150126204532_remove_shortcode_from_urls.rb b/source/db/migrate/20150126204532_remove_shortcode_from_urls.rb new file mode 100644 index 0000000..b73eca2 --- /dev/null +++ b/source/db/migrate/20150126204532_remove_shortcode_from_urls.rb @@ -0,0 +1,5 @@ +class RemoveShortcodeFromUrls < ActiveRecord::Migration + def change + remove_column :urls, :shortcode, :string + end +end diff --git a/source/db/migrate/20150127175240_add_count_to_urls.rb b/source/db/migrate/20150127175240_add_count_to_urls.rb new file mode 100644 index 0000000..4b3d247 --- /dev/null +++ b/source/db/migrate/20150127175240_add_count_to_urls.rb @@ -0,0 +1,5 @@ +class AddCountToUrls < ActiveRecord::Migration + def change + add_column :urls, :click_count, :integer, default: 0 + end +end diff --git a/source/db/schema.rb b/source/db/schema.rb new file mode 100644 index 0000000..e2ce234 --- /dev/null +++ b/source/db/schema.rb @@ -0,0 +1,23 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20150127175240) do + + create_table "urls", force: true do |t| + t.string "destination" + t.datetime "created_at" + t.datetime "updated_at" + t.integer "click_count", default: 0 + end + +end diff --git a/source/db/seeds.rb b/source/db/seeds.rb index 4edb1e8..f908ccb 100644 --- a/source/db/seeds.rb +++ b/source/db/seeds.rb @@ -5,3 +5,12 @@ # # cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }]) # Mayor.create(name: 'Emanuel', city: cities.first) + +Url.delete_all + +Url.create! destination: "covermymeds.com" +Url.create! destination: "https://www.google.com/" +Url.create! destination: "https://www.stackoverflow.com/" +Url.create! destination: "https://www.mint.com/" +Url.create! destination: "http://weather.com/" +Url.create! destination: "wikipedia.org" diff --git a/source/spec/controllers/urls_controller_spec.rb b/source/spec/controllers/urls_controller_spec.rb new file mode 100644 index 0000000..7014ddd --- /dev/null +++ b/source/spec/controllers/urls_controller_spec.rb @@ -0,0 +1,159 @@ +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to specify the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. +# +# Compared to earlier versions of this generator, there is very limited use of +# stubs and message expectations in this spec. Stubs are only used when there +# is no simpler way to get a handle on the object needed for the example. +# Message expectations are only used when there is no simpler way to specify +# that an instance is receiving a specific message. + +RSpec.describe UrlsController, :type => :controller do + + # This should return the minimal set of attributes required to create a valid + # Url. As you add validations to Url, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + skip("Add a hash of attributes valid for your model") + } + + let(:invalid_attributes) { + skip("Add a hash of attributes invalid for your model") + } + + # This should return the minimal set of values that should be in the session + # in order to pass any filters (e.g. authentication) defined in + # UrlsController. Be sure to keep this updated too. + let(:valid_session) { {} } + + describe "GET index" do + it "assigns all urls as @urls" do + url = Url.create! valid_attributes + get :index, {}, valid_session + expect(assigns(:urls)).to eq([url]) + end + end + + describe "GET show" do + it "assigns the requested url as @url" do + url = Url.create! valid_attributes + get :show, {:id => url.to_param}, valid_session + expect(assigns(:url)).to eq(url) + end + end + + describe "GET new" do + it "assigns a new url as @url" do + get :new, {}, valid_session + expect(assigns(:url)).to be_a_new(Url) + end + end + + describe "GET edit" do + it "assigns the requested url as @url" do + url = Url.create! valid_attributes + get :edit, {:id => url.to_param}, valid_session + expect(assigns(:url)).to eq(url) + end + end + + describe "POST create" do + describe "with valid params" do + it "creates a new Url" do + expect { + post :create, {:url => valid_attributes}, valid_session + }.to change(Url, :count).by(1) + end + + it "assigns a newly created url as @url" do + post :create, {:url => valid_attributes}, valid_session + expect(assigns(:url)).to be_a(Url) + expect(assigns(:url)).to be_persisted + end + + it "redirects to the created url" do + post :create, {:url => valid_attributes}, valid_session + expect(response).to redirect_to(Url.last) + end + end + + describe "with invalid params" do + it "assigns a newly created but unsaved url as @url" do + post :create, {:url => invalid_attributes}, valid_session + expect(assigns(:url)).to be_a_new(Url) + end + + it "re-renders the 'new' template" do + post :create, {:url => invalid_attributes}, valid_session + expect(response).to render_template("new") + end + end + end + + describe "PUT update" do + describe "with valid params" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested url" do + url = Url.create! valid_attributes + put :update, {:id => url.to_param, :url => new_attributes}, valid_session + url.reload + skip("Add assertions for updated state") + end + + it "assigns the requested url as @url" do + url = Url.create! valid_attributes + put :update, {:id => url.to_param, :url => valid_attributes}, valid_session + expect(assigns(:url)).to eq(url) + end + + it "redirects to the url" do + url = Url.create! valid_attributes + put :update, {:id => url.to_param, :url => valid_attributes}, valid_session + expect(response).to redirect_to(url) + end + end + + describe "with invalid params" do + it "assigns the url as @url" do + url = Url.create! valid_attributes + put :update, {:id => url.to_param, :url => invalid_attributes}, valid_session + expect(assigns(:url)).to eq(url) + end + + it "re-renders the 'edit' template" do + url = Url.create! valid_attributes + put :update, {:id => url.to_param, :url => invalid_attributes}, valid_session + expect(response).to render_template("edit") + end + end + end + + describe "DELETE destroy" do + it "destroys the requested url" do + url = Url.create! valid_attributes + expect { + delete :destroy, {:id => url.to_param}, valid_session + }.to change(Url, :count).by(-1) + end + + it "redirects to the urls list" do + url = Url.create! valid_attributes + delete :destroy, {:id => url.to_param}, valid_session + expect(response).to redirect_to(urls_url) + end + end + +end diff --git a/source/spec/helpers/urls_helper_spec.rb b/source/spec/helpers/urls_helper_spec.rb new file mode 100644 index 0000000..bbafaf3 --- /dev/null +++ b/source/spec/helpers/urls_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the UrlsHelper. For example: +# +# describe UrlsHelper 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 UrlsHelper, :type => :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/source/spec/models/url_spec.rb b/source/spec/models/url_spec.rb new file mode 100644 index 0000000..209ca4c --- /dev/null +++ b/source/spec/models/url_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Url, :type => :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/source/spec/requests/urls_spec.rb b/source/spec/requests/urls_spec.rb new file mode 100644 index 0000000..cd1976c --- /dev/null +++ b/source/spec/requests/urls_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +RSpec.describe "Urls", :type => :request do + describe "GET /urls" do + it "works! (now write some real specs)" do + get urls_path + expect(response).to have_http_status(200) + end + end +end diff --git a/source/spec/routing/urls_routing_spec.rb b/source/spec/routing/urls_routing_spec.rb new file mode 100644 index 0000000..d4ec06f --- /dev/null +++ b/source/spec/routing/urls_routing_spec.rb @@ -0,0 +1,35 @@ +require "rails_helper" + +RSpec.describe UrlsController, :type => :routing do + describe "routing" do + + it "routes to #index" do + expect(:get => "/urls").to route_to("urls#index") + end + + it "routes to #new" do + expect(:get => "/urls/new").to route_to("urls#new") + end + + it "routes to #show" do + expect(:get => "/urls/1").to route_to("urls#show", :id => "1") + end + + it "routes to #edit" do + expect(:get => "/urls/1/edit").to route_to("urls#edit", :id => "1") + end + + it "routes to #create" do + expect(:post => "/urls").to route_to("urls#create") + end + + it "routes to #update" do + expect(:put => "/urls/1").to route_to("urls#update", :id => "1") + end + + it "routes to #destroy" do + expect(:delete => "/urls/1").to route_to("urls#destroy", :id => "1") + end + + end +end diff --git a/source/spec/views/urls/edit.html.erb_spec.rb b/source/spec/views/urls/edit.html.erb_spec.rb new file mode 100644 index 0000000..3003a53 --- /dev/null +++ b/source/spec/views/urls/edit.html.erb_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +RSpec.describe "urls/edit", :type => :view do + before(:each) do + @url = assign(:url, Url.create!( + :shortcode => "MyString", + :destination => "MyString" + )) + end + + it "renders the edit url form" do + render + + assert_select "form[action=?][method=?]", url_path(@url), "post" do + + assert_select "input#url_shortcode[name=?]", "url[shortcode]" + + assert_select "input#url_destination[name=?]", "url[destination]" + end + end +end diff --git a/source/spec/views/urls/index.html.erb_spec.rb b/source/spec/views/urls/index.html.erb_spec.rb new file mode 100644 index 0000000..1fbc0ce --- /dev/null +++ b/source/spec/views/urls/index.html.erb_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +RSpec.describe "urls/index", :type => :view do + before(:each) do + assign(:urls, [ + Url.create!( + :shortcode => "Shortcode", + :destination => "Destination" + ), + Url.create!( + :shortcode => "Shortcode", + :destination => "Destination" + ) + ]) + end + + it "renders a list of urls" do + render + assert_select "tr>td", :text => "Shortcode".to_s, :count => 2 + assert_select "tr>td", :text => "Destination".to_s, :count => 2 + end +end diff --git a/source/spec/views/urls/new.html.erb_spec.rb b/source/spec/views/urls/new.html.erb_spec.rb new file mode 100644 index 0000000..f04b5f7 --- /dev/null +++ b/source/spec/views/urls/new.html.erb_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +RSpec.describe "urls/new", :type => :view do + before(:each) do + assign(:url, Url.new( + :shortcode => "MyString", + :destination => "MyString" + )) + end + + it "renders new url form" do + render + + assert_select "form[action=?][method=?]", urls_path, "post" do + + assert_select "input#url_shortcode[name=?]", "url[shortcode]" + + assert_select "input#url_destination[name=?]", "url[destination]" + end + end +end diff --git a/source/spec/views/urls/show.html.erb_spec.rb b/source/spec/views/urls/show.html.erb_spec.rb new file mode 100644 index 0000000..4b0727d --- /dev/null +++ b/source/spec/views/urls/show.html.erb_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +RSpec.describe "urls/show", :type => :view do + before(:each) do + @url = assign(:url, Url.create!( + :shortcode => "Shortcode", + :destination => "Destination" + )) + end + + it "renders attributes in" do + render + expect(rendered).to match(/Shortcode/) + expect(rendered).to match(/Destination/) + end +end