commit 03399ab51c0d12af33a24134db66804a8dda0074 Author: Andrew Tomaka Date: Tue Jul 21 10:33:31 2015 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2de85d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +db/*.db +config/app.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0695621 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,37 @@ +stages: + - test + - build + - deploy + +test: + stage: test + script: + - apt-get update -qy + - apt-get install -y nodejs libqtwebkit-dev qt4-qmake sqlite3 libsqlite3-dev xvfb + - bundle install --path /cache + - RACK_ENV=test bundle exec rake db:create + - RACK_ENV=test bundle exec rake db:migrate + - RACK_ENV=test xvfb-run -a bundle exec rspec + tags: + - rails +build: + stage: build + script: + - docker build -t atomaka/link-share . + except: + - tags + tags: + - docker +deploy: + stage: deploy + script: + - VERSION=$(git describe --tags) + - docker build -t atomaka/link-share . + - docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:$VERSION + - docker tag atomaka/link-share:latest docker.atomaka.com/atomaka/link-share:latest + - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD -e me@atomaka.com docker.atomaka.com + - docker push docker.atomaka.com/atomaka/link-share + only: + - tags + tags: + - docker diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..b1b25a5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.2.2 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..39d0b60 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +FROM alpine:latest + +RUN export LANG=en_US.UTF-8 && \ + export LANGUAGE=en_US.UTF-8 && \ + export LC_ALL=en_US.UTF-8 + +RUN apk update \ + && apk add build-base ruby-dev sqlite-dev \ + && apk add ruby ruby-bundler ruby-io-console \ + && rm -rf /var/cache/apk* + +WORKDIR /app +COPY Gemfile* ./ +RUN bundle install --path=vendor/bundle --jobs=4 --without=development test +COPY . /app + +CMD bundle exec rackup -o 0.0.0.0 diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..f144423 --- /dev/null +++ b/Gemfile @@ -0,0 +1,26 @@ +source 'https://rubygems.org' + +gem 'activerecord' +gem 'sinatra' +gem 'sqlite3' + +gem 'sinatra-activerecord' +gem 'sinatra-contrib', require: false +gem 'sinatra-flash' +gem 'validate_url' + +gem 'slim' + +gem 'bigdecimal' +# alpine linux does not include a method to determine the timezone +gem 'tzinfo-data' + +group :development do + gem 'rspec' + gem 'capybara-webkit' + gem 'factory_girl' + gem 'database_cleaner' + gem 'launchy' + gem 'pry' + gem 'rerun' +end diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..84fc6a7 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,143 @@ +GEM + remote: https://rubygems.org/ + specs: + activemodel (4.2.3) + activesupport (= 4.2.3) + builder (~> 3.1) + activerecord (4.2.3) + activemodel (= 4.2.3) + activesupport (= 4.2.3) + arel (~> 6.0) + activesupport (4.2.3) + i18n (~> 0.7) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.3.8) + arel (6.0.2) + backports (3.6.6) + bigdecimal (1.2.7) + builder (3.2.2) + capybara (2.5.0) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) + capybara-webkit (1.7.1) + capybara (>= 2.3.0, < 2.6.0) + json + celluloid (0.16.0) + timers (~> 4.0.0) + coderay (1.1.0) + database_cleaner (1.4.1) + diff-lcs (1.2.5) + factory_girl (4.5.0) + activesupport (>= 3.0.0) + ffi (1.9.10) + hitimes (1.2.2) + i18n (0.7.0) + json (1.8.3) + launchy (2.4.3) + addressable (~> 2.3) + listen (2.10.1) + celluloid (~> 0.16.0) + rb-fsevent (>= 0.9.3) + rb-inotify (>= 0.9) + method_source (0.8.2) + mime-types (3.0) + mime-types-data (~> 3.2015) + mime-types-data (3.2015.1120) + mini_portile2 (2.0.0) + minitest (5.7.0) + multi_json (1.11.2) + nokogiri (1.6.7) + mini_portile2 (~> 2.0.0.rc2) + pry (0.10.1) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + rack (1.6.4) + rack-protection (1.5.3) + rack + rack-test (0.6.3) + rack (>= 1.0) + rb-fsevent (0.9.5) + rb-inotify (0.9.5) + ffi (>= 0.5.0) + rerun (0.10.0) + listen (~> 2.7, >= 2.7.3) + rspec (3.4.0) + rspec-core (~> 3.4.0) + rspec-expectations (~> 3.4.0) + rspec-mocks (~> 3.4.0) + rspec-core (3.4.1) + rspec-support (~> 3.4.0) + rspec-expectations (3.4.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-mocks (3.4.0) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.4.0) + rspec-support (3.4.1) + sinatra (1.4.6) + rack (~> 1.4) + rack-protection (~> 1.4) + tilt (>= 1.3, < 3) + sinatra-activerecord (2.0.6) + activerecord (>= 3.2) + sinatra (~> 1.0) + sinatra-contrib (1.4.6) + backports (>= 2.0) + multi_json + rack-protection + rack-test + sinatra (~> 1.4.0) + tilt (>= 1.3, < 3) + sinatra-flash (0.3.0) + sinatra (>= 1.0.0) + slim (3.0.6) + temple (~> 0.7.3) + tilt (>= 1.3.3, < 2.1) + slop (3.6.0) + sqlite3 (1.3.10) + temple (0.7.6) + thread_safe (0.3.5) + tilt (2.0.1) + timers (4.0.1) + hitimes + tzinfo (1.2.2) + thread_safe (~> 0.1) + tzinfo-data (1.2015.7) + tzinfo (>= 1.0.0) + validate_url (1.0.2) + activemodel (>= 3.0.0) + addressable + xpath (2.0.0) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + activerecord + bigdecimal + capybara-webkit + database_cleaner + factory_girl + launchy + pry + rerun + rspec + sinatra + sinatra-activerecord + sinatra-contrib + sinatra-flash + slim + sqlite3 + tzinfo-data + validate_url + +BUNDLED WITH + 1.10.6 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1c389df --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +NAME = atomaka/docker-linkshare +CONTAINER = sinatra-linkshare +DOMAIN = linkshare.atomaka.com +PORT = 10081 +DATABASE = /home/atomaka/linkshare.db + +all: build + +build: + docker build -t $(NAME) . + +clean: + docker rm -f $(CONTAINER) + +deploy: clean + touch $(DATABASE) + docker run -e VIRTUAL_HOST=$(DOMAIN) -d -p $(PORT):9292 -v $(DATABASE):/app/db/linkshare.db --name=$(CONTAINER) --restart=always $(NAME) + docker exec $(CONTAINER) rake db:migrate + +update: + git pull origin master diff --git a/README.md b/README.md new file mode 100644 index 0000000..26921b4 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +[![build status](https://git.atomaka.com/ci/projects/2/status.png?ref=master)](https://git.atomaka.com/ci/projects/2?ref=master) + +# link-share diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..1134fa0 --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +require './app' +require 'sinatra/activerecord/rake' + +desc "Starts the development server" +task :serve do + `bundle exec rerun -b rackup` +end diff --git a/app.rb b/app.rb new file mode 100644 index 0000000..4ab628e --- /dev/null +++ b/app.rb @@ -0,0 +1,89 @@ +require './config/init' + +get '/manage' do + protected! + + @links = get_links(params[:status]) + + slim :manage +end + +get '/new' do + protected! + + @link = Link.new + + slim :new +end + +post '/' do + protected! + + @link = Link.new(params[:link]) + + if @link.save + flash[:success] = 'Link has been created' + redirect '/manage' + else + flash.now[:danger] = 'Did not pass validations' + slim :new + end +end + +get '/send' do + protected! + + @link = Link.find(params[:id]) + + @link.mark_sent + + flash[:success] = 'Link has been marked as sent' + redirect '/manage' +end + +get '/destroy' do + protected! + + @link = Link.find(params[:id]) + + if @link.sent? + flash[:warning] = 'Cannot delete sent link' + else + @link.destroy + flash[:success] = 'Link has been deleted' + end + + redirect '/manage' +end + +get '/' do + slim :calendar +end + +get '/events' do + start = params[:start] + finish = params[:end] + + json serialize(Link.calendar(start, finish)) +end + +private + def get_links(status) + if status == 'sent' + Link.sent + elsif status == 'all' + Link.all + else + Link.unsent + end + end + + def serialize(events) + events.map do |event| + { + title: event.title, + url: event.url, + start: event.sent_at.in_time_zone('Eastern Time (US & Canada)') + } + end + end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..76a6edf --- /dev/null +++ b/config.ru @@ -0,0 +1,2 @@ +require './app' +run Sinatra::Application diff --git a/config/app.yml.sample b/config/app.yml.sample new file mode 100644 index 0000000..321ffd1 --- /dev/null +++ b/config/app.yml.sample @@ -0,0 +1,3 @@ +secret: YOUR_SECRET +users: + USERNAME: PASSWORD diff --git a/config/environments.rb b/config/environments.rb new file mode 100644 index 0000000..95c9e8f --- /dev/null +++ b/config/environments.rb @@ -0,0 +1,15 @@ +set :database, 'sqlite3:db/linkshare.db' + +configure :production do + config_file 'config/app.yml' +end + +configure :development do + set :show_exceptions, true + config_file 'config/app.yml' +end + +configure :test do + set :database, 'sqlite3:db/test.db' + config_file 'spec/fixtures/app.yml' +end diff --git a/config/init.rb b/config/init.rb new file mode 100644 index 0000000..c3d1df5 --- /dev/null +++ b/config/init.rb @@ -0,0 +1,17 @@ +require 'rubygems' +require 'bundler' +require 'bundler/setup' +require 'sinatra/config_file' +require 'sinatra/json' +Bundler.require + +set :root, File.dirname('..') + +require_relative 'environments' + +require_relative '../models/link' +require_relative '../helpers/application_helper' + +config_file 'config/app.yml' + +use Rack::Session::Cookie, secret: settings.secret diff --git a/db/.gitkeep b/db/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/db/migrate/20150721142956_add_links.rb b/db/migrate/20150721142956_add_links.rb new file mode 100644 index 0000000..38c7b00 --- /dev/null +++ b/db/migrate/20150721142956_add_links.rb @@ -0,0 +1,11 @@ +class AddLinks < ActiveRecord::Migration + def change + create_table :links do |t| + t.string :title + t.string :link + t.datetime :sent_at + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20150721201947_change_link_to_url.rb b/db/migrate/20150721201947_change_link_to_url.rb new file mode 100644 index 0000000..0d09320 --- /dev/null +++ b/db/migrate/20150721201947_change_link_to_url.rb @@ -0,0 +1,5 @@ +class ChangeLinkToUrl < ActiveRecord::Migration + def change + rename_column :links, :link, :url + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..a96a86e --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,24 @@ +# 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: 20150721201947) do + + create_table "links", force: :cascade do |t| + t.string "title" + t.string "url" + t.datetime "sent_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end diff --git a/helpers/application_helper.rb b/helpers/application_helper.rb new file mode 100644 index 0000000..678ba9d --- /dev/null +++ b/helpers/application_helper.rb @@ -0,0 +1,20 @@ +helpers do + def protected! + return if authorized? + headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"' + halt 401, "Not authorized\n" + end + + def authorized? + @auth ||= Rack::Auth::Basic::Request.new(request.env) + return unless @auth.provided? and @auth.basic? + + username, password = @auth.credentials + + user_exists?(username) && settings.users[username] == password + end + + def user_exists?(username) + settings.users.keys.include?(username) + end +end diff --git a/models/link.rb b/models/link.rb new file mode 100644 index 0000000..02128f5 --- /dev/null +++ b/models/link.rb @@ -0,0 +1,27 @@ +class Link < ActiveRecord::Base + validates :title, + presence: true + validates :url, + presence: true, + url: true, + uniqueness: true + + scope :sent, -> { where('sent_at IS NOT NULL').order('sent_at DESC') } + scope :unsent, -> { where('sent_at IS NULL').order('created_at ASC') } + scope :sent_after, ->(date) { where('sent_at > ?', date) } + scope :sent_before, ->(date) { where('sent_at < ?', date) } + scope :calendar, ->(start, finish) { sent_after(start).sent_before(finish) } + + def mark_sent + update_attribute(:sent_at, Time.now) + end + + def sent? + !!self.sent_at + end + + def url=(url) + url.chomp!('/') if url.respond_to?(:chomp) + write_attribute(:url, url) + end +end diff --git a/public/custom.css b/public/custom.css new file mode 100644 index 0000000..0e59572 --- /dev/null +++ b/public/custom.css @@ -0,0 +1,17 @@ +.link { + padding: 5px; + background: #fff; +} + +.link:nth-of-type(odd) { + background: #e7e7e7 +} + +.controls { + margin-bottom: 10px; + margin-top: 10px; +} + +.fc-time { + display: none; +} diff --git a/public/custom.js b/public/custom.js new file mode 100644 index 0000000..dfb933c --- /dev/null +++ b/public/custom.js @@ -0,0 +1,10 @@ +$(document).ready(function() { + $('#calendar').fullCalendar({ + header: { + left: 'prev,next today', + center: 'title', + right: '' + }, + events: '/events', + }); +}); diff --git a/spec/factories/link_factory.rb b/spec/factories/link_factory.rb new file mode 100644 index 0000000..92d9d02 --- /dev/null +++ b/spec/factories/link_factory.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :link do + sequence(:title) { |n| "Sequenced Title (#{n})" } + sequence(:url) { |n| "http://www.#{n}example#{n}.com/#{n}" } + end +end diff --git a/spec/features/admin/create_link_spec.rb b/spec/features/admin/create_link_spec.rb new file mode 100644 index 0000000..be06c32 --- /dev/null +++ b/spec/features/admin/create_link_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe 'Admin Create Links' do + context 'when not logged in' do + it 'should not allow access to new link form' do + visit '/new' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + before(:each) { basic_auth 'admin', 'password' } + + context 'with valid data' do + let(:link) { build(:link) } + + it 'should allow creation of a link' do + visit '/new' + fill_in :link_title, with: link.title + fill_in :link_url, with: link.url + click_button 'Submit' + + expect(page).to have_content 'Link has been created' + end + + it 'should list the new link' do + visit '/new' + fill_in :link_title, with: link.title + fill_in :link_url, with: link.url + click_button 'Submit' + + expect(page).to have_content link.title + end + end + + context 'with invalid data' do + let(:link) { build(:link, title: '') } + + it 'should not allow link with invalid data' do + visit '/new' + fill_in :link_title, with: link.title + fill_in :link_url, with: link.url + click_button 'Submit' + + expect(page).to have_content 'Did not pass validations' + end + end + end +end diff --git a/spec/features/admin/delete_link_spec.rb b/spec/features/admin/delete_link_spec.rb new file mode 100644 index 0000000..9b3426f --- /dev/null +++ b/spec/features/admin/delete_link_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe 'Admin Delete Links' do + context 'when not logged in' do + it 'should not allow access to delete links' do + visit '/send' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + let!(:links) { 10.times.collect { create(:link) } } + before(:each) { basic_auth 'admin', 'password' } + + it 'should allow deleting of a link' do + visit '/manage' + first('a', text: 'Delete').click + expect(page).to have_content('Link has been deleted') + end + + it 'should remove deleted link all lists' do + link = create(:link) + + visit '/manage' + + find('a', text: link.title) + .find(:xpath, '../..') + .find('a', text: 'Delete') + .click + + visit '/manage?status=all' + + expect(page).to_not have_content(link.title) + end + end +end diff --git a/spec/features/admin/list_links_spec.rb b/spec/features/admin/list_links_spec.rb new file mode 100644 index 0000000..62878bb --- /dev/null +++ b/spec/features/admin/list_links_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe 'Admin List Links' do + context 'when not logged in' do + it 'should not allow access to list links' do + visit '/manage' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + let!(:sent_link) { create(:link, sent_at: Time.now) } + let!(:links) { 10.times.collect { create(:link) } } + before(:each) { basic_auth 'admin', 'password' } + + context 'unsent' do + it 'should show unsent links by default' do + visit '/manage' + + expect(page).to_not have_content(sent_link.title) + links.each { |link| expect(page).to have_content(link.title) } + end + + it 'should only list unsent links' do + visit '/manage' + first('a', text: 'Unsent Links').click + + expect(page).to_not have_content(sent_link.title) + links.each { |link| expect(page).to have_content(link.title) } + end + end + + context 'sent' do + it 'should only list sent links' do + visit '/manage' + first('a', text: 'Sent Links').click + + expect(page).to have_content(sent_link.title) + links.each { |link| expect(page).to_not have_content(link.title) } + end + end + + context 'all' do + it 'should list all links' do + visit '/manage' + first('a', text: 'All Links').click + + expect(page).to have_content(sent_link.title) + links.each { |link| expect(page).to have_content(link.title) } + end + end + end +end diff --git a/spec/features/admin/send_link_spec.rb b/spec/features/admin/send_link_spec.rb new file mode 100644 index 0000000..571a583 --- /dev/null +++ b/spec/features/admin/send_link_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe 'Admin Send Links' do + context 'when not logged in' do + it 'should not allow access to send links' do + visit '/send' + expect(page.status_code).to be(401) + end + end + + context 'when logged in' do + let!(:links) { 10.times.collect { create(:link) } } + before(:each) { basic_auth 'admin', 'password' } + + it 'should allow sending of a link' do + visit '/manage' + first('a', text: 'Send').click + expect(page).to have_content('Link has been marked as sent') + end + + it 'should remove sent link from unsent list' do + link = create(:link) + + visit '/manage' + + find('a', text: link.title) + .find(:xpath, '../..') + .find('a', text: 'Send') + .click + + expect(page).to_not have_content(link.title) + end + end +end diff --git a/spec/features/authorization_spec.rb b/spec/features/authorization_spec.rb new file mode 100644 index 0000000..db825a1 --- /dev/null +++ b/spec/features/authorization_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe 'Authorization' do + context 'when not logged in' do + it 'should not allow access to manage' do + visit '/manage' + expect(page.status_code).to be(401) + end + + it 'should not allow access to the create form' do + visit '/new' + expect(page.status_code).to be(401) + end + + it 'should not allow access to send' do + visit '/send' + expect(page.status_code).to be(401) + end + + it 'should not allow access to delete' do + visit '/destroy' + expect(page.status_code).to be(401) + end + end + + it 'should allow accessing the home page' do + visit '/' + expect(page.status_code).to be(200) + end + + it 'should allow accessing the events page' do + visit '/events' + expect(page.status_code).to be(200) + end + + context 'when logging in' do + context 'with incorrect credentials' do + before(:each) { basic_auth 'baduser', 'badpassword' } + + it 'should allow not allow access' do + visit '/manage' + expect(page.status_code).to be(401) + end + end + + context 'with correct credentials' do + before(:each) { basic_auth 'admin', 'password' } + + it 'should allow access' do + visit '/manage' + + expect(page.status_code).to be(200) + end + end + end +end diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb new file mode 100644 index 0000000..c6c11ef --- /dev/null +++ b/spec/features/calendar_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'Calendar View' do + OVER_ONE_MONTH = 60*60*24*40 + + let!(:showing_link) { create(:link, sent_at: Time.now) } + let!(:before_link) { create(:link, sent_at: Time.now - OVER_ONE_MONTH) } + let!(:after_link) { create(:link, sent_at: Time.now + OVER_ONE_MONTH) } + + it 'should only show links for the current month', js: true do + visit '/' + + expect(page).to have_content(showing_link.title) + expect(page).to_not have_content(before_link.title) + expect(page).to_not have_content(after_link.title) + end +end diff --git a/spec/fixtures/app.yml b/spec/fixtures/app.yml new file mode 100644 index 0000000..49e8dc2 --- /dev/null +++ b/spec/fixtures/app.yml @@ -0,0 +1,3 @@ +secret: my_secret_something_blan +users: + admin: password diff --git a/spec/models/link_spec.rb b/spec/models/link_spec.rb new file mode 100644 index 0000000..682b87f --- /dev/null +++ b/spec/models/link_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Link do + let(:link) { build(:link) } + + context 'with valid data' do + it 'should be valid' do + expect(link).to be_valid + end + end + + context 'with invalid data' do + it 'should not be valid with blank title' do + link.title = '' + + expect(link).to be_invalid + end + + it 'should not be valid with a blank url' do + link.url = '' + + expect(link).to be_invalid + end + + it 'should not be valid with a bad url' do + link.url = 'bad' + + expect(link).to be_invalid + end + + it 'should not be valid with a duplicate url' do + create(:link, url: link.url) + + expect(link).to be_invalid + end + end + + context '#mark_sent' do + it 'should set a time to sent_at' do + link.mark_sent + + expect(link.sent_at).to_not be(nil) + end + end + + context '#sent?' do + context 'when already sent' do + it 'should respond with true' do + link.mark_sent + expect(link.sent?).to be(true) + end + + it 'should respond with false' do + expect(link.sent?).to be(false) + end + end + end + + context '#url' do + it 'removes a trailing slash' do + link.url = 'http://www.url.com/' + expect(link.url).to eq('http://www.url.com') + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..4f7ca7e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,72 @@ +require 'rack/test' +require 'rspec' +require 'capybara/rspec' +require 'capybara/webkit' +require 'factory_girl' +require 'database_cleaner' + +ENV['RACK_ENV'] = 'test' + +require File.expand_path '../../app.rb', __FILE__ + +ActiveRecord::Migration.maintain_test_schema! + +module TestingMixin + include Rack::Test::Methods + include RSpec::Matchers + include Capybara::DSL + include FactoryGirl::Syntax::Methods + + Capybara.app = Sinatra::Application + Capybara.javascript_driver = :webkit + Capybara.asset_host = 'http://localhost:3000' + + FactoryGirl.definition_file_paths = %w{./factories ./test/factories ./spec/factories} + FactoryGirl.find_definitions + + def app() Sinatra::Application end + + def basic_auth(username, password) + if page.driver.respond_to?(:basic_auth) + page.driver.basic_auth(username, password) + elsif page.driver.respond_to?(:basic_authorize) + page.driver.basic_authorize(username, password) + elsif page.driver.respond_to?(:browser) && page.driver.browser.respond_to?(:basic_authorize) + page.driver.browser.basic_authorize(username, password) + else + raise "I don't know how to log in!" + end + end +end + +Capybara::Webkit.configure do |config| + config.allow_unknown_urls +end + +RSpec.configure do |config| + config.include TestingMixin + + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :truncation + end + + config.before(:each, :js => true) do + DatabaseCleaner.strategy = :truncation + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.append_after(:each) do + DatabaseCleaner.clean + Capybara.reset_sessions! + end + + config.order = :random + Kernel.srand config.seed +end diff --git a/views/calendar.slim b/views/calendar.slim new file mode 100644 index 0000000..114bf52 --- /dev/null +++ b/views/calendar.slim @@ -0,0 +1 @@ +#calendar diff --git a/views/form_errors.slim b/views/form_errors.slim new file mode 100644 index 0000000..bad3ced --- /dev/null +++ b/views/form_errors.slim @@ -0,0 +1,4 @@ +- if @link.errors.any? + ul + - @link.errors.full_messages.each do |error| + li= error diff --git a/views/layout.slim b/views/layout.slim new file mode 100644 index 0000000..c8f8515 --- /dev/null +++ b/views/layout.slim @@ -0,0 +1,33 @@ +doctype html +html + head + title Links App + link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" + link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.css" + link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.print.css" media="print" + link rel="stylesheet" href="/custom.css" + body + .navbar.navbar-default.navbar-static-top.navbar-custom + .container + .navbar-header + button.navbar-toggle.collapsed type='button' data-toggle='collapse' data-target='.navbar-collapse' + span.sr-only Toggle navigation + span.icon-bar + span.icon-bar + span.icon-bar + a href="/" class="navbar-brand" Links + .collapse.navbar-collapse + ul.nav.navbar-nav + li + a href="/manage" Manage + ul.nav.navbar-nav.navbar-right + .container + - unless flash.empty? + - flash.each_key do |key| + .alert class="alert-#{key}" role="alert"= flash[key] + == yield + script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js" + script src="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" + script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js" + script src="//cdnjs.cloudflare.com/ajax/libs/fullcalendar/2.3.2/fullcalendar.min.js" + script src="custom.js" diff --git a/views/link_list.slim b/views/link_list.slim new file mode 100644 index 0000000..8917f64 --- /dev/null +++ b/views/link_list.slim @@ -0,0 +1,14 @@ +.links +- @links.each do |link| + .row.link + .col-md-9 + a href="#{link.url}" #{link.title} + .col-md-3 + - if link.sent_at + = link.sent_at + - else + ul.list-inline style="margin: 0" + li + a href="/send?id=#{link.id}" Send + li + a href="/destroy?id=#{link.id}" Delete diff --git a/views/manage.slim b/views/manage.slim new file mode 100644 index 0000000..72aa3d2 --- /dev/null +++ b/views/manage.slim @@ -0,0 +1,15 @@ +.row.controls + .btn-toolbar.pull-right + a href="/manage?status=unsent" class="btn btn-primary" Unsent Links + a href="/manage?status=sent" class="btn btn-primary" Sent Links + a href="/manage?status=all" class="btn btn-primary" All Links + a href="/new" class="btn btn-primary" New Link + +== slim :link_list + +.row.controls + .btn-toolbar + a href="/manage?status=unsent" class="btn btn-primary" Unsent Links + a href="/manage?status=sent" class="btn btn-primary" Sent Links + a href="/manage?status=all" class="btn btn-primary" All Links + a href="/new" class="btn btn-primary" New Link diff --git a/views/new.slim b/views/new.slim new file mode 100644 index 0000000..bc9255e --- /dev/null +++ b/views/new.slim @@ -0,0 +1,9 @@ +== slim :form_errors +form action="/" method="post" + .form-group + label for="link_title" Title + input type="text" name="link[title]" id="link_title" class="form-control" value="#{@link.title}" + .form-group + label for="link_url" Link + input type="text" name="link[url]" id="link_url" class="form-control" value="#{@link.url}" + button type="submit" class="btn btn-primary" Submit